3 Коммитов

Автор SHA1 Сообщение Дата
Luc Didry
eeba8e627d 📝 — Add a word about installation instructions on binaries download page
Некоторые проверки не удались
API / build (push) Failing after 1m52s
Web App CI Master / master-ci (push) Failing after 3m32s
Server CI Master / master-ci (push) Failing after 13m22s
CodeQL / Analyze (go) (push) Has been skipped
CodeQL / Analyze (javascript) (push) Has been skipped
Scorecards supply-chain security / Scorecard analysis (push) Has been skipped
2026-01-12 17:10:32 +01:00
Luc Didry
630bd908ed 📝 — Add a word about no SSO in Mostlymatter 2026-01-12 16:59:49 +01:00
Luc Didry
f43717aaa0 🚚 — Officially fork Mattermost
- 👷🩹 — Add limitless patch and GitlabCI recipe
- 📝 — Add instructions on how to use the patch from Framasoft
- 💄 — Add logo, thanks to Geoffrey Dorne
- 👷 — ARM64 cross-compilation in CI
2025-06-19 09:47:16 +02:00
3149 изменённых файлов: 81193 добавлений и 244419 удалений

64
.github/ISSUE_TEMPLATE/bug_report.yml поставляемый
Просмотреть файл

@@ -1,64 +0,0 @@
name: Bug report
description: Create a report about an issue you found
title: "[Bug]: "
labels: "Bug Report/Open"
body:
- type: checkboxes
attributes:
label: Before you file a bug report
description: Please ensure you can confirm the following
options:
- label: I have checked the [issue tracker](https://github.com/mattermost/mattermost/issues) and have not found an issue that matches the one I'm filing.
required: true
- label: "This issue is not a troubleshooting question. Troubleshooting questions go here: https://forum.mattermost.com/c/trouble-shoot/16."
required: true
- label: "This issue is not a feature request. You can request features and make product suggestions here: https://mattermost.uservoice.com/forums/306457-general/."
required: true
- label: This issue reproduces on one of the [currently supported server versions](https://docs.mattermost.com/about/mattermost-server-releases.html#latest-releases).
required: true
- label: I have read the [contribution guidelines](https://github.com/mattermost/mattermost/blob/master/CONTRIBUTING.md) and the [Mattermost Handbook Contribution Guidelines](https://handbook.mattermost.com/contributors/contributors/guidelines/contribution-guidelines).
required: true
- type: input
attributes:
label: Mattermost Server Version
description: |
What version of the Mattermost server are you using? You can find it by going to [Main Menu] > [About Mattermost].
validations:
required: true
- type: input
attributes:
label: Operating System
description: |
What operating system does this issue occur on?
Example: Windows 10
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: |
Include a clear description of the steps taken to reproduce this issue.
It is also helpful to attach a screenshot or video if applicable.
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: Include a clear description of what you expect to happen.
validations:
required: true
- type: textarea
attributes:
label: Observed behavior
description: Include a clear description of what actually happens.
validations:
required: true
- type: textarea
attributes:
label: Log Output
description: Please include output from the log files.
render: shell
- type: textarea
attributes:
label: Additional Information
description: If you have anything else to add to the ticket, you may do so here.

Просмотреть файл

@@ -1,2 +0,0 @@
node_modules/
.env

Просмотреть файл

@@ -1,50 +0,0 @@
name: Calculate Cypress Results
description: Calculate Cypress test results with optional merge of retest results
author: Mattermost
inputs:
original-results-path:
description: Path to the original Cypress results directory (e.g., e2e-tests/cypress/results)
required: true
retest-results-path:
description: Path to the retest Cypress results directory (optional - if not provided, only calculates from original)
required: false
write-merged:
description: Whether to write merged results back to the original directory (default true)
required: false
default: "true"
outputs:
# Merge outputs
merged:
description: Whether merge was performed (true/false)
# Calculation outputs (same as calculate-cypress-test-results)
passed:
description: Number of passed tests
failed:
description: Number of failed tests
pending:
description: Number of pending/skipped tests
total_specs:
description: Total number of spec files
commit_status_message:
description: Message for commit status (e.g., "X failed, Y passed (Z spec files)")
failed_specs:
description: Comma-separated list of failed spec files (for retest)
failed_specs_count:
description: Number of failed spec files
failed_tests:
description: Markdown table rows of failed tests (for GitHub summary)
total:
description: Total number of tests (passed + failed)
pass_rate:
description: Pass rate percentage (e.g., "100.00")
color:
description: Color for webhook based on pass rate (green=100%, yellow=99%+, orange=98%+, red=<98%)
test_duration:
description: Wall-clock test duration (earliest start to latest end across all specs, formatted as "Xm Ys")
runs:
using: node24
main: dist/index.js

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@@ -1,15 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
moduleFileExtensions: ["ts", "js"],
transform: {
"^.+\\.ts$": [
"ts-jest",
{
useESM: false,
},
],
},
};

9136
.github/actions/calculate-cypress-results/package-lock.json сгенерированный поставляемый

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -1,27 +0,0 @@
{
"name": "calculate-cypress-results",
"private": true,
"version": "0.1.0",
"main": "dist/index.js",
"scripts": {
"build": "tsup",
"prettier": "npx prettier --write \"src/**/*.ts\"",
"local-action": "local-action . src/main.ts .env",
"test": "jest --verbose",
"test:watch": "jest --watch --verbose",
"test:silent": "jest --silent",
"tsc": "tsc -b"
},
"dependencies": {
"@actions/core": "3.0.0"
},
"devDependencies": {
"@github/local-action": "7.0.0",
"@types/jest": "30.0.0",
"@types/node": "25.2.0",
"jest": "30.2.0",
"ts-jest": "29.4.6",
"tsup": "8.5.1",
"typescript": "5.9.3"
}
}

Просмотреть файл

@@ -1,3 +0,0 @@
import { run } from "./main";
run();

Просмотреть файл

@@ -1,101 +0,0 @@
import * as core from "@actions/core";
import {
loadSpecFiles,
mergeResults,
writeMergedResults,
calculateResultsFromSpecs,
} from "./merge";
export async function run(): Promise<void> {
const originalPath = core.getInput("original-results-path", {
required: true,
});
const retestPath = core.getInput("retest-results-path"); // Optional
const shouldWriteMerged = core.getInput("write-merged") !== "false"; // Default true
core.info(`Original results: ${originalPath}`);
core.info(`Retest results: ${retestPath || "(not provided)"}`);
let merged = false;
let specs;
if (retestPath) {
// Check if retest path has results
const retestSpecs = await loadSpecFiles(retestPath);
if (retestSpecs.length > 0) {
core.info(`Found ${retestSpecs.length} retest spec files`);
// Merge results
core.info("Merging results...");
const mergeResult = await mergeResults(originalPath, retestPath);
specs = mergeResult.specs;
merged = true;
core.info(`Retested specs: ${mergeResult.retestFiles.join(", ")}`);
core.info(`Total merged specs: ${specs.length}`);
// Write merged results back to original directory
if (shouldWriteMerged) {
core.info("Writing merged results to original directory...");
const writeResult = await writeMergedResults(
originalPath,
retestPath,
);
core.info(`Updated files: ${writeResult.updatedFiles.length}`);
core.info(
`Removed duplicates: ${writeResult.removedFiles.length}`,
);
}
} else {
core.warning(
`No retest results found at ${retestPath}, using original only`,
);
specs = await loadSpecFiles(originalPath);
}
} else {
core.info("No retest path provided, using original results only");
specs = await loadSpecFiles(originalPath);
}
core.info(`Calculating results from ${specs.length} spec files...`);
// Handle case where no results found
if (specs.length === 0) {
core.setFailed("No Cypress test results found");
return;
}
// Calculate all outputs from final results
const calc = calculateResultsFromSpecs(specs);
// Log results
core.startGroup("Final Results");
core.info(`Passed: ${calc.passed}`);
core.info(`Failed: ${calc.failed}`);
core.info(`Pending: ${calc.pending}`);
core.info(`Total: ${calc.total}`);
core.info(`Pass Rate: ${calc.passRate}%`);
core.info(`Color: ${calc.color}`);
core.info(`Spec Files: ${calc.totalSpecs}`);
core.info(`Failed Specs Count: ${calc.failedSpecsCount}`);
core.info(`Commit Status Message: ${calc.commitStatusMessage}`);
core.info(`Failed Specs: ${calc.failedSpecs || "none"}`);
core.info(`Test Duration: ${calc.testDuration}`);
core.endGroup();
// Set all outputs
core.setOutput("merged", merged.toString());
core.setOutput("passed", calc.passed);
core.setOutput("failed", calc.failed);
core.setOutput("pending", calc.pending);
core.setOutput("total_specs", calc.totalSpecs);
core.setOutput("commit_status_message", calc.commitStatusMessage);
core.setOutput("failed_specs", calc.failedSpecs);
core.setOutput("failed_specs_count", calc.failedSpecsCount);
core.setOutput("failed_tests", calc.failedTests);
core.setOutput("total", calc.total);
core.setOutput("pass_rate", calc.passRate);
core.setOutput("color", calc.color);
core.setOutput("test_duration", calc.testDuration);
}

Просмотреть файл

@@ -1,271 +0,0 @@
import { calculateResultsFromSpecs } from "./merge";
import type { ParsedSpecFile, MochawesomeResult } from "./types";
/**
* Helper to create a mochawesome result for testing
*/
function createMochawesomeResult(
specFile: string,
tests: { title: string; state: "passed" | "failed" | "pending" }[],
): MochawesomeResult {
return {
stats: {
suites: 1,
tests: tests.length,
passes: tests.filter((t) => t.state === "passed").length,
pending: tests.filter((t) => t.state === "pending").length,
failures: tests.filter((t) => t.state === "failed").length,
start: new Date().toISOString(),
end: new Date().toISOString(),
duration: 1000,
testsRegistered: tests.length,
passPercent: 0,
pendingPercent: 0,
other: 0,
hasOther: false,
skipped: 0,
hasSkipped: false,
},
results: [
{
uuid: "uuid-1",
title: specFile,
fullFile: `/app/e2e-tests/cypress/tests/integration/${specFile}`,
file: `tests/integration/${specFile}`,
beforeHooks: [],
afterHooks: [],
tests: tests.map((t, i) => ({
title: t.title,
fullTitle: `${specFile} > ${t.title}`,
timedOut: null,
duration: 500,
state: t.state,
speed: "fast",
pass: t.state === "passed",
fail: t.state === "failed",
pending: t.state === "pending",
context: null,
code: "",
err: t.state === "failed" ? { message: "Test failed" } : {},
uuid: `test-uuid-${i}`,
parentUUID: "uuid-1",
isHook: false,
skipped: false,
})),
suites: [],
passes: tests
.filter((t) => t.state === "passed")
.map((_, i) => `test-uuid-${i}`),
failures: tests
.filter((t) => t.state === "failed")
.map((_, i) => `test-uuid-${i}`),
pending: tests
.filter((t) => t.state === "pending")
.map((_, i) => `test-uuid-${i}`),
skipped: [],
duration: 1000,
root: true,
rootEmpty: false,
_timeout: 60000,
},
],
};
}
function createParsedSpecFile(
specFile: string,
tests: { title: string; state: "passed" | "failed" | "pending" }[],
): ParsedSpecFile {
return {
filePath: `/path/to/${specFile}.json`,
specPath: `tests/integration/${specFile}`,
result: createMochawesomeResult(specFile, tests),
};
}
describe("calculateResultsFromSpecs", () => {
it("should calculate all outputs correctly for passing results", () => {
const specs: ParsedSpecFile[] = [
createParsedSpecFile("login.spec.ts", [
{
title: "should login with valid credentials",
state: "passed",
},
]),
createParsedSpecFile("messaging.spec.ts", [
{ title: "should send a message", state: "passed" },
]),
];
const calc = calculateResultsFromSpecs(specs);
expect(calc.passed).toBe(2);
expect(calc.failed).toBe(0);
expect(calc.pending).toBe(0);
expect(calc.total).toBe(2);
expect(calc.passRate).toBe("100.00");
expect(calc.color).toBe("#43A047"); // green
expect(calc.totalSpecs).toBe(2);
expect(calc.failedSpecs).toBe("");
expect(calc.failedSpecsCount).toBe(0);
expect(calc.commitStatusMessage).toBe("100% passed (2), 2 specs");
});
it("should calculate all outputs correctly for results with failures", () => {
const specs: ParsedSpecFile[] = [
createParsedSpecFile("login.spec.ts", [
{
title: "should login with valid credentials",
state: "passed",
},
]),
createParsedSpecFile("channels.spec.ts", [
{ title: "should create a channel", state: "failed" },
]),
];
const calc = calculateResultsFromSpecs(specs);
expect(calc.passed).toBe(1);
expect(calc.failed).toBe(1);
expect(calc.pending).toBe(0);
expect(calc.total).toBe(2);
expect(calc.passRate).toBe("50.00");
expect(calc.color).toBe("#F44336"); // red
expect(calc.totalSpecs).toBe(2);
expect(calc.failedSpecs).toBe("tests/integration/channels.spec.ts");
expect(calc.failedSpecsCount).toBe(1);
expect(calc.commitStatusMessage).toBe(
"50.0% passed (1/2), 1 failed, 2 specs",
);
expect(calc.failedTests).toContain("should create a channel");
});
it("should handle pending tests correctly", () => {
const specs: ParsedSpecFile[] = [
createParsedSpecFile("login.spec.ts", [
{ title: "should login", state: "passed" },
{ title: "should logout", state: "pending" },
]),
];
const calc = calculateResultsFromSpecs(specs);
expect(calc.passed).toBe(1);
expect(calc.failed).toBe(0);
expect(calc.pending).toBe(1);
expect(calc.total).toBe(1); // Total excludes pending
expect(calc.passRate).toBe("100.00");
});
it("should limit failed tests to 10 entries", () => {
const specs: ParsedSpecFile[] = [
createParsedSpecFile("big-test.spec.ts", [
{ title: "test 1", state: "failed" },
{ title: "test 2", state: "failed" },
{ title: "test 3", state: "failed" },
{ title: "test 4", state: "failed" },
{ title: "test 5", state: "failed" },
{ title: "test 6", state: "failed" },
{ title: "test 7", state: "failed" },
{ title: "test 8", state: "failed" },
{ title: "test 9", state: "failed" },
{ title: "test 10", state: "failed" },
{ title: "test 11", state: "failed" },
{ title: "test 12", state: "failed" },
]),
];
const calc = calculateResultsFromSpecs(specs);
expect(calc.failed).toBe(12);
expect(calc.failedTests).toContain("...and 2 more failed tests");
});
});
describe("merge simulation", () => {
it("should produce correct results when merging original with retest", () => {
// Simulate original: 2 passed, 1 failed
const originalSpecs: ParsedSpecFile[] = [
createParsedSpecFile("login.spec.ts", [
{ title: "should login", state: "passed" },
]),
createParsedSpecFile("messaging.spec.ts", [
{ title: "should send message", state: "passed" },
]),
createParsedSpecFile("channels.spec.ts", [
{ title: "should create channel", state: "failed" },
]),
];
// Verify original has failure
const originalCalc = calculateResultsFromSpecs(originalSpecs);
expect(originalCalc.passed).toBe(2);
expect(originalCalc.failed).toBe(1);
expect(originalCalc.passRate).toBe("66.67");
// Simulate retest: channels.spec.ts now passes
const retestSpec = createParsedSpecFile("channels.spec.ts", [
{ title: "should create channel", state: "passed" },
]);
// Simulate merge: replace original channels.spec.ts with retest
const specMap = new Map<string, ParsedSpecFile>();
for (const spec of originalSpecs) {
specMap.set(spec.specPath, spec);
}
specMap.set(retestSpec.specPath, retestSpec);
const mergedSpecs = Array.from(specMap.values());
// Calculate final results
const finalCalc = calculateResultsFromSpecs(mergedSpecs);
expect(finalCalc.passed).toBe(3);
expect(finalCalc.failed).toBe(0);
expect(finalCalc.pending).toBe(0);
expect(finalCalc.total).toBe(3);
expect(finalCalc.passRate).toBe("100.00");
expect(finalCalc.color).toBe("#43A047"); // green
expect(finalCalc.totalSpecs).toBe(3);
expect(finalCalc.failedSpecs).toBe("");
expect(finalCalc.failedSpecsCount).toBe(0);
expect(finalCalc.commitStatusMessage).toBe("100% passed (3), 3 specs");
});
it("should handle case where retest still fails", () => {
// Original: 1 passed, 1 failed
const originalSpecs: ParsedSpecFile[] = [
createParsedSpecFile("login.spec.ts", [
{ title: "should login", state: "passed" },
]),
createParsedSpecFile("channels.spec.ts", [
{ title: "should create channel", state: "failed" },
]),
];
// Retest: channels.spec.ts still fails
const retestSpec = createParsedSpecFile("channels.spec.ts", [
{ title: "should create channel", state: "failed" },
]);
// Merge
const specMap = new Map<string, ParsedSpecFile>();
for (const spec of originalSpecs) {
specMap.set(spec.specPath, spec);
}
specMap.set(retestSpec.specPath, retestSpec);
const mergedSpecs = Array.from(specMap.values());
const finalCalc = calculateResultsFromSpecs(mergedSpecs);
expect(finalCalc.passed).toBe(1);
expect(finalCalc.failed).toBe(1);
expect(finalCalc.passRate).toBe("50.00");
expect(finalCalc.color).toBe("#F44336"); // red
expect(finalCalc.failedSpecs).toBe(
"tests/integration/channels.spec.ts",
);
expect(finalCalc.failedSpecsCount).toBe(1);
});
});

Просмотреть файл

@@ -1,358 +0,0 @@
import * as fs from "fs/promises";
import * as path from "path";
import type {
MochawesomeResult,
ParsedSpecFile,
CalculationResult,
FailedTest,
TestItem,
SuiteItem,
ResultItem,
} from "./types";
/**
* Find all JSON files in a directory recursively
*/
async function findJsonFiles(dir: string): Promise<string[]> {
const files: string[] = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const subFiles = await findJsonFiles(fullPath);
files.push(...subFiles);
} else if (entry.isFile() && entry.name.endsWith(".json")) {
files.push(fullPath);
}
}
} catch {
// Directory doesn't exist or not accessible
}
return files;
}
/**
* Parse a mochawesome JSON file
*/
async function parseSpecFile(filePath: string): Promise<ParsedSpecFile | null> {
try {
const content = await fs.readFile(filePath, "utf8");
const result: MochawesomeResult = JSON.parse(content);
// Extract spec path from results[0].file
const specPath = result.results?.[0]?.file;
if (!specPath) {
return null;
}
return {
filePath,
specPath,
result,
};
} catch {
return null;
}
}
/**
* Extract all tests from a result recursively
*/
function getAllTests(result: MochawesomeResult): TestItem[] {
const tests: TestItem[] = [];
function extractFromSuite(suite: SuiteItem | ResultItem) {
tests.push(...(suite.tests || []));
for (const nestedSuite of suite.suites || []) {
extractFromSuite(nestedSuite);
}
}
for (const resultItem of result.results || []) {
extractFromSuite(resultItem);
}
return tests;
}
/**
* Get color based on pass rate
*/
function getColor(passRate: number): string {
if (passRate === 100) {
return "#43A047"; // green
} else if (passRate >= 99) {
return "#FFEB3B"; // yellow
} else if (passRate >= 98) {
return "#FF9800"; // orange
} else {
return "#F44336"; // red
}
}
/**
* Calculate results from parsed spec files
*/
/**
* Format milliseconds as "Xm Ys"
*/
function formatDuration(ms: number): string {
const totalSeconds = Math.round(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}m ${seconds}s`;
}
export function calculateResultsFromSpecs(
specs: ParsedSpecFile[],
): CalculationResult {
let passed = 0;
let failed = 0;
let pending = 0;
const failedSpecsSet = new Set<string>();
const failedTestsList: FailedTest[] = [];
for (const spec of specs) {
const tests = getAllTests(spec.result);
for (const test of tests) {
if (test.state === "passed") {
passed++;
} else if (test.state === "failed") {
failed++;
failedSpecsSet.add(spec.specPath);
failedTestsList.push({
title: test.title,
file: spec.specPath,
});
} else if (test.state === "pending") {
pending++;
}
}
}
// Compute test duration from earliest start to latest end across all specs
let earliestStart: number | null = null;
let latestEnd: number | null = null;
for (const spec of specs) {
const { start, end } = spec.result.stats;
if (start) {
const startMs = new Date(start).getTime();
if (earliestStart === null || startMs < earliestStart) {
earliestStart = startMs;
}
}
if (end) {
const endMs = new Date(end).getTime();
if (latestEnd === null || endMs > latestEnd) {
latestEnd = endMs;
}
}
}
const testDurationMs =
earliestStart !== null && latestEnd !== null
? latestEnd - earliestStart
: 0;
const testDuration = formatDuration(testDurationMs);
const totalSpecs = specs.length;
const failedSpecs = Array.from(failedSpecsSet).join(",");
const failedSpecsCount = failedSpecsSet.size;
// Build failed tests markdown table (limit to 10)
let failedTests = "";
const uniqueFailedTests = failedTestsList.filter(
(test, index, self) =>
index ===
self.findIndex(
(t) => t.title === test.title && t.file === test.file,
),
);
if (uniqueFailedTests.length > 0) {
const limitedTests = uniqueFailedTests.slice(0, 10);
failedTests = limitedTests
.map((t) => {
const escapedTitle = t.title
.replace(/`/g, "\\`")
.replace(/\|/g, "\\|");
return `| ${escapedTitle} | ${t.file} |`;
})
.join("\n");
if (uniqueFailedTests.length > 10) {
const remaining = uniqueFailedTests.length - 10;
failedTests += `\n| _...and ${remaining} more failed tests_ | |`;
}
} else if (failed > 0) {
failedTests = "| Unable to parse failed tests | - |";
}
// Calculate totals and pass rate
// Pass rate = passed / (passed + failed), excluding pending
const total = passed + failed;
const passRate = total > 0 ? ((passed * 100) / total).toFixed(2) : "0.00";
const color = getColor(parseFloat(passRate));
// Build commit status message
const rate = total > 0 ? (passed * 100) / total : 0;
const rateStr = rate === 100 ? "100%" : `${rate.toFixed(1)}%`;
const specSuffix = totalSpecs > 0 ? `, ${totalSpecs} specs` : "";
const commitStatusMessage =
rate === 100
? `${rateStr} passed (${passed})${specSuffix}`
: `${rateStr} passed (${passed}/${total}), ${failed} failed${specSuffix}`;
return {
passed,
failed,
pending,
totalSpecs,
commitStatusMessage,
failedSpecs,
failedSpecsCount,
failedTests,
total,
passRate,
color,
testDuration,
};
}
/**
* Load all spec files from a mochawesome results directory
*/
export async function loadSpecFiles(
resultsPath: string,
): Promise<ParsedSpecFile[]> {
// Mochawesome results are at: results/mochawesome-report/json/tests/
const mochawesomeDir = path.join(
resultsPath,
"mochawesome-report",
"json",
"tests",
);
const jsonFiles = await findJsonFiles(mochawesomeDir);
const specs: ParsedSpecFile[] = [];
for (const file of jsonFiles) {
const parsed = await parseSpecFile(file);
if (parsed) {
specs.push(parsed);
}
}
return specs;
}
/**
* Merge original and retest results
* - For each spec in retest, replace the matching spec in original
* - Keep original specs that are not in retest
*/
export async function mergeResults(
originalPath: string,
retestPath: string,
): Promise<{
specs: ParsedSpecFile[];
retestFiles: string[];
mergedCount: number;
}> {
const originalSpecs = await loadSpecFiles(originalPath);
const retestSpecs = await loadSpecFiles(retestPath);
// Build a map of original specs by spec path
const specMap = new Map<string, ParsedSpecFile>();
for (const spec of originalSpecs) {
specMap.set(spec.specPath, spec);
}
// Replace with retest results
const retestFiles: string[] = [];
for (const retestSpec of retestSpecs) {
specMap.set(retestSpec.specPath, retestSpec);
retestFiles.push(retestSpec.specPath);
}
return {
specs: Array.from(specMap.values()),
retestFiles,
mergedCount: retestSpecs.length,
};
}
/**
* Write merged results back to the original directory
* This updates the original JSON files with retest results
*/
export async function writeMergedResults(
originalPath: string,
retestPath: string,
): Promise<{ updatedFiles: string[]; removedFiles: string[] }> {
const mochawesomeDir = path.join(
originalPath,
"mochawesome-report",
"json",
"tests",
);
const retestMochawesomeDir = path.join(
retestPath,
"mochawesome-report",
"json",
"tests",
);
const originalJsonFiles = await findJsonFiles(mochawesomeDir);
const retestJsonFiles = await findJsonFiles(retestMochawesomeDir);
const updatedFiles: string[] = [];
const removedFiles: string[] = [];
// For each retest file, find and replace the original
for (const retestFile of retestJsonFiles) {
const retestSpec = await parseSpecFile(retestFile);
if (!retestSpec) continue;
const specPath = retestSpec.specPath;
// Find all original files with matching spec path
// Prefer nested path (under integration/), remove flat duplicates
let nestedFile: string | null = null;
const flatFiles: string[] = [];
for (const origFile of originalJsonFiles) {
const origSpec = await parseSpecFile(origFile);
if (origSpec && origSpec.specPath === specPath) {
if (origFile.includes("/integration/")) {
nestedFile = origFile;
} else {
flatFiles.push(origFile);
}
}
}
// Update the nested file (proper location) or first flat file if no nested
const retestContent = await fs.readFile(retestFile, "utf8");
if (nestedFile) {
await fs.writeFile(nestedFile, retestContent);
updatedFiles.push(nestedFile);
// Remove flat duplicates
for (const flatFile of flatFiles) {
await fs.unlink(flatFile);
removedFiles.push(flatFile);
}
} else if (flatFiles.length > 0) {
await fs.writeFile(flatFiles[0], retestContent);
updatedFiles.push(flatFiles[0]);
}
}
return { updatedFiles, removedFiles };
}

Просмотреть файл

@@ -1,139 +0,0 @@
/**
* Mochawesome result structure for a single spec file
*/
export interface MochawesomeResult {
stats: MochawesomeStats;
results: ResultItem[];
}
export interface MochawesomeStats {
suites: number;
tests: number;
passes: number;
pending: number;
failures: number;
start: string;
end: string;
duration: number;
testsRegistered: number;
passPercent: number;
pendingPercent: number;
other: number;
hasOther: boolean;
skipped: number;
hasSkipped: boolean;
}
export interface ResultItem {
uuid: string;
title: string;
fullFile: string;
file: string;
beforeHooks: Hook[];
afterHooks: Hook[];
tests: TestItem[];
suites: SuiteItem[];
passes: string[];
failures: string[];
pending: string[];
skipped: string[];
duration: number;
root: boolean;
rootEmpty: boolean;
_timeout: number;
}
export interface SuiteItem {
uuid: string;
title: string;
fullFile: string;
file: string;
beforeHooks: Hook[];
afterHooks: Hook[];
tests: TestItem[];
suites: SuiteItem[];
passes: string[];
failures: string[];
pending: string[];
skipped: string[];
duration: number;
root: boolean;
rootEmpty: boolean;
_timeout: number;
}
export interface TestItem {
title: string;
fullTitle: string;
timedOut: boolean | null;
duration: number;
state: "passed" | "failed" | "pending";
speed: string | null;
pass: boolean;
fail: boolean;
pending: boolean;
context: string | null;
code: string;
err: TestError;
uuid: string;
parentUUID: string;
isHook: boolean;
skipped: boolean;
}
export interface TestError {
message?: string;
estack?: string;
diff?: string | null;
}
export interface Hook {
title: string;
fullTitle: string;
timedOut: boolean | null;
duration: number;
state: string | null;
speed: string | null;
pass: boolean;
fail: boolean;
pending: boolean;
context: string | null;
code: string;
err: TestError;
uuid: string;
parentUUID: string;
isHook: boolean;
skipped: boolean;
}
/**
* Parsed spec file with its path and results
*/
export interface ParsedSpecFile {
filePath: string;
specPath: string;
result: MochawesomeResult;
}
/**
* Calculation result outputs
*/
export interface CalculationResult {
passed: number;
failed: number;
pending: number;
totalSpecs: number;
commitStatusMessage: string;
failedSpecs: string;
failedSpecsCount: number;
failedTests: string;
total: number;
passRate: string;
color: string;
testDuration: string;
}
export interface FailedTest {
title: string;
file: string;
}

Просмотреть файл

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Просмотреть файл

@@ -1 +0,0 @@
{"root":["./src/index.ts","./src/main.ts","./src/merge.ts","./src/types.ts"],"version":"5.9.3"}

Просмотреть файл

@@ -1,13 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["cjs"],
target: "node24",
clean: true,
minify: false,
sourcemap: false,
splitting: false,
bundle: true,
noExternal: [/.*/],
});

Просмотреть файл

@@ -1,2 +0,0 @@
node_modules/
.env

Просмотреть файл

@@ -1,53 +0,0 @@
name: Calculate Playwright Results
description: Calculate Playwright test results with optional merge of retest results
author: Mattermost
inputs:
original-results-path:
description: Path to the original Playwright results.json file
required: true
retest-results-path:
description: Path to the retest Playwright results.json file (optional - if not provided, only calculates from original)
required: false
output-path:
description: Path to write the merged results.json file (defaults to original-results-path)
required: false
outputs:
# Merge outputs
merged:
description: Whether merge was performed (true/false)
# Calculation outputs (same as calculate-playwright-test-results)
passed:
description: Number of passed tests (not including flaky)
failed:
description: Number of failed tests
flaky:
description: Number of flaky tests (failed initially but passed on retry)
skipped:
description: Number of skipped tests
total_specs:
description: Total number of spec files
commit_status_message:
description: Message for commit status (e.g., "X failed, Y passed (Z spec files)")
failed_specs:
description: Comma-separated list of failed spec files (for retest)
failed_specs_count:
description: Number of failed spec files
failed_tests:
description: Markdown table rows of failed tests (for GitHub summary)
total:
description: Total number of tests (passed + flaky + failed)
pass_rate:
description: Pass rate percentage (e.g., "100.00")
passing:
description: Number of passing tests (passed + flaky)
color:
description: Color for webhook based on pass rate (green=100%, yellow=99%+, orange=98%+, red=<98%)
test_duration:
description: Test execution duration from stats (formatted as "Xm Ys")
runs:
using: node24
main: dist/index.js

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@@ -1,6 +0,0 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
moduleFileExtensions: ["ts", "js"],
};

9136
.github/actions/calculate-playwright-results/package-lock.json сгенерированный поставляемый

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -1,27 +0,0 @@
{
"name": "calculate-playwright-results",
"private": true,
"version": "0.1.0",
"main": "dist/index.js",
"scripts": {
"build": "tsup",
"prettier": "npx prettier --write \"src/**/*.ts\"",
"local-action": "local-action . src/main.ts .env",
"test": "jest --verbose",
"test:watch": "jest --watch --verbose",
"test:silent": "jest --silent",
"tsc": "tsc -b"
},
"dependencies": {
"@actions/core": "3.0.0"
},
"devDependencies": {
"@github/local-action": "7.0.0",
"@types/jest": "30.0.0",
"@types/node": "25.2.0",
"jest": "30.2.0",
"ts-jest": "29.4.6",
"tsup": "8.5.1",
"typescript": "5.9.3"
}
}

Просмотреть файл

@@ -1,3 +0,0 @@
import { run } from "./main";
run();

Просмотреть файл

@@ -1,123 +0,0 @@
import * as core from "@actions/core";
import * as fs from "fs/promises";
import type { PlaywrightResults } from "./types";
import { mergeResults, calculateResults } from "./merge";
export async function run(): Promise<void> {
const originalPath = core.getInput("original-results-path", {
required: true,
});
const retestPath = core.getInput("retest-results-path"); // Optional
const outputPath = core.getInput("output-path") || originalPath;
core.info(`Original results: ${originalPath}`);
core.info(`Retest results: ${retestPath || "(not provided)"}`);
core.info(`Output path: ${outputPath}`);
// Check if original file exists
const originalExists = await fs
.access(originalPath)
.then(() => true)
.catch(() => false);
if (!originalExists) {
core.setFailed(`Original results not found at ${originalPath}`);
return;
}
// Read original file
core.info("Reading original results...");
const originalContent = await fs.readFile(originalPath, "utf8");
const original: PlaywrightResults = JSON.parse(originalContent);
core.info(
`Original: ${original.suites.length} suites, stats: ${JSON.stringify(original.stats)}`,
);
// Check if retest path is provided and exists
let finalResults: PlaywrightResults;
let merged = false;
if (retestPath) {
const retestExists = await fs
.access(retestPath)
.then(() => true)
.catch(() => false);
if (retestExists) {
// Read retest file and merge
core.info("Reading retest results...");
const retestContent = await fs.readFile(retestPath, "utf8");
const retest: PlaywrightResults = JSON.parse(retestContent);
core.info(
`Retest: ${retest.suites.length} suites, stats: ${JSON.stringify(retest.stats)}`,
);
// Merge results
core.info("Merging results at suite level...");
const mergeResult = mergeResults(original, retest);
finalResults = mergeResult.merged;
merged = true;
core.info(`Retested specs: ${mergeResult.retestFiles.join(", ")}`);
core.info(
`Kept ${original.suites.length - mergeResult.retestFiles.length} original suites`,
);
core.info(`Added ${retest.suites.length} retest suites`);
core.info(`Total merged suites: ${mergeResult.totalSuites}`);
// Write merged results
core.info(`Writing merged results to ${outputPath}...`);
await fs.writeFile(
outputPath,
JSON.stringify(finalResults, null, 2),
);
} else {
core.warning(
`Retest results not found at ${retestPath}, using original only`,
);
finalResults = original;
}
} else {
core.info("No retest path provided, using original results only");
finalResults = original;
}
// Calculate all outputs from final results
const calc = calculateResults(finalResults);
// Log results
core.startGroup("Final Results");
core.info(`Passed: ${calc.passed}`);
core.info(`Failed: ${calc.failed}`);
core.info(`Flaky: ${calc.flaky}`);
core.info(`Skipped: ${calc.skipped}`);
core.info(`Passing (passed + flaky): ${calc.passing}`);
core.info(`Total: ${calc.total}`);
core.info(`Pass Rate: ${calc.passRate}%`);
core.info(`Color: ${calc.color}`);
core.info(`Spec Files: ${calc.totalSpecs}`);
core.info(`Failed Specs Count: ${calc.failedSpecsCount}`);
core.info(`Commit Status Message: ${calc.commitStatusMessage}`);
core.info(`Failed Specs: ${calc.failedSpecs || "none"}`);
core.info(`Test Duration: ${calc.testDuration}`);
core.endGroup();
// Set all outputs
core.setOutput("merged", merged.toString());
core.setOutput("passed", calc.passed);
core.setOutput("failed", calc.failed);
core.setOutput("flaky", calc.flaky);
core.setOutput("skipped", calc.skipped);
core.setOutput("total_specs", calc.totalSpecs);
core.setOutput("commit_status_message", calc.commitStatusMessage);
core.setOutput("failed_specs", calc.failedSpecs);
core.setOutput("failed_specs_count", calc.failedSpecsCount);
core.setOutput("failed_tests", calc.failedTests);
core.setOutput("total", calc.total);
core.setOutput("pass_rate", calc.passRate);
core.setOutput("passing", calc.passing);
core.setOutput("color", calc.color);
core.setOutput("test_duration", calc.testDuration);
}

Просмотреть файл

@@ -1,509 +0,0 @@
import { mergeResults, computeStats, calculateResults } from "./merge";
import type { PlaywrightResults, Suite } from "./types";
describe("mergeResults", () => {
const createSuite = (file: string, tests: { status: string }[]): Suite => ({
title: file,
file,
column: 0,
line: 0,
specs: [
{
title: "test spec",
ok: true,
tags: [],
tests: tests.map((t) => ({
timeout: 60000,
annotations: [],
expectedStatus: "passed",
projectId: "chrome",
projectName: "chrome",
results: [
{
workerIndex: 0,
parallelIndex: 0,
status: t.status,
duration: 1000,
errors: [],
stdout: [],
stderr: [],
retry: 0,
startTime: new Date().toISOString(),
annotations: [],
},
],
})),
},
],
});
it("should keep original suites not in retest", () => {
const original: PlaywrightResults = {
config: {},
suites: [
createSuite("spec1.ts", [{ status: "passed" }]),
createSuite("spec2.ts", [{ status: "failed" }]),
createSuite("spec3.ts", [{ status: "passed" }]),
],
stats: {
startTime: new Date().toISOString(),
duration: 10000,
expected: 2,
unexpected: 1,
skipped: 0,
flaky: 0,
},
};
const retest: PlaywrightResults = {
config: {},
suites: [createSuite("spec2.ts", [{ status: "passed" }])],
stats: {
startTime: new Date().toISOString(),
duration: 5000,
expected: 1,
unexpected: 0,
skipped: 0,
flaky: 0,
},
};
const result = mergeResults(original, retest);
expect(result.totalSuites).toBe(3);
expect(result.retestFiles).toEqual(["spec2.ts"]);
expect(result.merged.suites.map((s) => s.file)).toEqual([
"spec1.ts",
"spec3.ts",
"spec2.ts",
]);
});
it("should compute correct stats from merged suites", () => {
const original: PlaywrightResults = {
config: {},
suites: [
createSuite("spec1.ts", [{ status: "passed" }]),
createSuite("spec2.ts", [{ status: "failed" }]),
],
stats: {
startTime: new Date().toISOString(),
duration: 10000,
expected: 1,
unexpected: 1,
skipped: 0,
flaky: 0,
},
};
const retest: PlaywrightResults = {
config: {},
suites: [createSuite("spec2.ts", [{ status: "passed" }])],
stats: {
startTime: new Date().toISOString(),
duration: 5000,
expected: 1,
unexpected: 0,
skipped: 0,
flaky: 0,
},
};
const result = mergeResults(original, retest);
expect(result.stats.expected).toBe(2);
expect(result.stats.unexpected).toBe(0);
expect(result.stats.duration).toBe(15000);
});
});
describe("computeStats", () => {
it("should count flaky tests correctly", () => {
const suites: Suite[] = [
{
title: "spec1.ts",
file: "spec1.ts",
column: 0,
line: 0,
specs: [
{
title: "flaky test",
ok: true,
tags: [],
tests: [
{
timeout: 60000,
annotations: [],
expectedStatus: "passed",
projectId: "chrome",
projectName: "chrome",
results: [
{
workerIndex: 0,
parallelIndex: 0,
status: "failed",
duration: 1000,
errors: [],
stdout: [],
stderr: [],
retry: 0,
startTime: new Date().toISOString(),
annotations: [],
},
{
workerIndex: 0,
parallelIndex: 0,
status: "passed",
duration: 1000,
errors: [],
stdout: [],
stderr: [],
retry: 1,
startTime: new Date().toISOString(),
annotations: [],
},
],
},
],
},
],
},
];
const stats = computeStats(suites);
expect(stats.expected).toBe(0);
expect(stats.flaky).toBe(1);
expect(stats.unexpected).toBe(0);
});
});
describe("calculateResults", () => {
const createSuiteWithSpec = (
file: string,
specTitle: string,
testResults: { status: string; retry: number }[],
): Suite => ({
title: file,
file,
column: 0,
line: 0,
specs: [
{
title: specTitle,
ok: testResults[testResults.length - 1].status === "passed",
tags: [],
tests: [
{
timeout: 60000,
annotations: [],
expectedStatus: "passed",
projectId: "chrome",
projectName: "chrome",
results: testResults.map((r) => ({
workerIndex: 0,
parallelIndex: 0,
status: r.status,
duration: 1000,
errors:
r.status === "failed"
? [{ message: "error" }]
: [],
stdout: [],
stderr: [],
retry: r.retry,
startTime: new Date().toISOString(),
annotations: [],
})),
location: {
file,
line: 10,
column: 5,
},
},
],
},
],
});
it("should calculate all outputs correctly for passing results", () => {
const results: PlaywrightResults = {
config: {},
suites: [
createSuiteWithSpec("login.spec.ts", "should login", [
{ status: "passed", retry: 0 },
]),
createSuiteWithSpec(
"messaging.spec.ts",
"should send message",
[{ status: "passed", retry: 0 }],
),
],
stats: {
startTime: new Date().toISOString(),
duration: 5000,
expected: 2,
unexpected: 0,
skipped: 0,
flaky: 0,
},
};
const calc = calculateResults(results);
expect(calc.passed).toBe(2);
expect(calc.failed).toBe(0);
expect(calc.flaky).toBe(0);
expect(calc.skipped).toBe(0);
expect(calc.total).toBe(2);
expect(calc.passing).toBe(2);
expect(calc.passRate).toBe("100.00");
expect(calc.color).toBe("#43A047"); // green
expect(calc.totalSpecs).toBe(2);
expect(calc.failedSpecs).toBe("");
expect(calc.failedSpecsCount).toBe(0);
expect(calc.commitStatusMessage).toBe("100% passed (2), 2 specs");
});
it("should calculate all outputs correctly for results with failures", () => {
const results: PlaywrightResults = {
config: {},
suites: [
createSuiteWithSpec("login.spec.ts", "should login", [
{ status: "passed", retry: 0 },
]),
createSuiteWithSpec(
"channels.spec.ts",
"should create channel",
[
{ status: "failed", retry: 0 },
{ status: "failed", retry: 1 },
{ status: "failed", retry: 2 },
],
),
],
stats: {
startTime: new Date().toISOString(),
duration: 10000,
expected: 1,
unexpected: 1,
skipped: 0,
flaky: 0,
},
};
const calc = calculateResults(results);
expect(calc.passed).toBe(1);
expect(calc.failed).toBe(1);
expect(calc.flaky).toBe(0);
expect(calc.total).toBe(2);
expect(calc.passing).toBe(1);
expect(calc.passRate).toBe("50.00");
expect(calc.color).toBe("#F44336"); // red
expect(calc.totalSpecs).toBe(2);
expect(calc.failedSpecs).toBe("channels.spec.ts");
expect(calc.failedSpecsCount).toBe(1);
expect(calc.commitStatusMessage).toBe(
"50.0% passed (1/2), 1 failed, 2 specs",
);
expect(calc.failedTests).toContain("should create channel");
});
});
describe("full integration: original with failure, retest passes", () => {
const createSuiteWithSpec = (
file: string,
specTitle: string,
testResults: { status: string; retry: number }[],
): Suite => ({
title: file,
file,
column: 0,
line: 0,
specs: [
{
title: specTitle,
ok: testResults[testResults.length - 1].status === "passed",
tags: [],
tests: [
{
timeout: 60000,
annotations: [],
expectedStatus: "passed",
projectId: "chrome",
projectName: "chrome",
results: testResults.map((r) => ({
workerIndex: 0,
parallelIndex: 0,
status: r.status,
duration: 1000,
errors:
r.status === "failed"
? [{ message: "error" }]
: [],
stdout: [],
stderr: [],
retry: r.retry,
startTime: new Date().toISOString(),
annotations: [],
})),
location: {
file,
line: 10,
column: 5,
},
},
],
},
],
});
it("should merge and calculate correctly when failed test passes on retest", () => {
// Original: 2 passed, 1 failed (channels.spec.ts)
const original: PlaywrightResults = {
config: {},
suites: [
createSuiteWithSpec("login.spec.ts", "should login", [
{ status: "passed", retry: 0 },
]),
createSuiteWithSpec(
"messaging.spec.ts",
"should send message",
[{ status: "passed", retry: 0 }],
),
createSuiteWithSpec(
"channels.spec.ts",
"should create channel",
[
{ status: "failed", retry: 0 },
{ status: "failed", retry: 1 },
{ status: "failed", retry: 2 },
],
),
],
stats: {
startTime: new Date().toISOString(),
duration: 18000,
expected: 2,
unexpected: 1,
skipped: 0,
flaky: 0,
},
};
// Retest: channels.spec.ts now passes
const retest: PlaywrightResults = {
config: {},
suites: [
createSuiteWithSpec(
"channels.spec.ts",
"should create channel",
[{ status: "passed", retry: 0 }],
),
],
stats: {
startTime: new Date().toISOString(),
duration: 3000,
expected: 1,
unexpected: 0,
skipped: 0,
flaky: 0,
},
};
// Step 1: Verify original has failure
const originalCalc = calculateResults(original);
expect(originalCalc.passed).toBe(2);
expect(originalCalc.failed).toBe(1);
expect(originalCalc.passRate).toBe("66.67");
// Step 2: Merge results
const mergeResult = mergeResults(original, retest);
// Step 3: Verify merge structure
expect(mergeResult.totalSuites).toBe(3);
expect(mergeResult.retestFiles).toEqual(["channels.spec.ts"]);
expect(mergeResult.merged.suites.map((s) => s.file)).toEqual([
"login.spec.ts",
"messaging.spec.ts",
"channels.spec.ts",
]);
// Step 4: Calculate final results
const finalCalc = calculateResults(mergeResult.merged);
// Step 5: Verify all outputs
expect(finalCalc.passed).toBe(3);
expect(finalCalc.failed).toBe(0);
expect(finalCalc.flaky).toBe(0);
expect(finalCalc.skipped).toBe(0);
expect(finalCalc.total).toBe(3);
expect(finalCalc.passing).toBe(3);
expect(finalCalc.passRate).toBe("100.00");
expect(finalCalc.color).toBe("#43A047"); // green
expect(finalCalc.totalSpecs).toBe(3);
expect(finalCalc.failedSpecs).toBe("");
expect(finalCalc.failedSpecsCount).toBe(0);
expect(finalCalc.commitStatusMessage).toBe("100% passed (3), 3 specs");
expect(finalCalc.failedTests).toBe("");
});
it("should handle case where retest still fails", () => {
// Original: 2 passed, 1 failed
const original: PlaywrightResults = {
config: {},
suites: [
createSuiteWithSpec("login.spec.ts", "should login", [
{ status: "passed", retry: 0 },
]),
createSuiteWithSpec(
"channels.spec.ts",
"should create channel",
[{ status: "failed", retry: 0 }],
),
],
stats: {
startTime: new Date().toISOString(),
duration: 10000,
expected: 1,
unexpected: 1,
skipped: 0,
flaky: 0,
},
};
// Retest: channels.spec.ts still fails
const retest: PlaywrightResults = {
config: {},
suites: [
createSuiteWithSpec(
"channels.spec.ts",
"should create channel",
[
{ status: "failed", retry: 0 },
{ status: "failed", retry: 1 },
],
),
],
stats: {
startTime: new Date().toISOString(),
duration: 5000,
expected: 0,
unexpected: 1,
skipped: 0,
flaky: 0,
},
};
const mergeResult = mergeResults(original, retest);
const finalCalc = calculateResults(mergeResult.merged);
expect(finalCalc.passed).toBe(1);
expect(finalCalc.failed).toBe(1);
expect(finalCalc.passRate).toBe("50.00");
expect(finalCalc.color).toBe("#F44336"); // red
expect(finalCalc.failedSpecs).toBe("channels.spec.ts");
expect(finalCalc.failedSpecsCount).toBe(1);
});
});

Просмотреть файл

@@ -1,304 +0,0 @@
import type {
PlaywrightResults,
Suite,
Test,
Stats,
MergeResult,
CalculationResult,
FailedTest,
} from "./types";
interface TestInfo {
title: string;
file: string;
finalStatus: string;
hadFailure: boolean;
}
/**
* Extract all tests from suites recursively with their info
*/
function getAllTestsWithInfo(suites: Suite[]): TestInfo[] {
const tests: TestInfo[] = [];
function extractFromSuite(suite: Suite) {
for (const spec of suite.specs || []) {
for (const test of spec.tests || []) {
if (!test.results || test.results.length === 0) {
continue;
}
const finalResult = test.results[test.results.length - 1];
const hadFailure = test.results.some(
(r) => r.status === "failed" || r.status === "timedOut",
);
tests.push({
title: spec.title || test.projectName,
file: test.location?.file || suite.file,
finalStatus: finalResult.status,
hadFailure,
});
}
}
for (const nestedSuite of suite.suites || []) {
extractFromSuite(nestedSuite);
}
}
for (const suite of suites) {
extractFromSuite(suite);
}
return tests;
}
/**
* Extract all tests from suites recursively
*/
function getAllTests(suites: Suite[]): Test[] {
const tests: Test[] = [];
function extractFromSuite(suite: Suite) {
for (const spec of suite.specs || []) {
tests.push(...spec.tests);
}
for (const nestedSuite of suite.suites || []) {
extractFromSuite(nestedSuite);
}
}
for (const suite of suites) {
extractFromSuite(suite);
}
return tests;
}
/**
* Compute stats from suites
*/
export function computeStats(
suites: Suite[],
originalStats?: Stats,
retestStats?: Stats,
): Stats {
const tests = getAllTests(suites);
let expected = 0;
let unexpected = 0;
let skipped = 0;
let flaky = 0;
for (const test of tests) {
if (!test.results || test.results.length === 0) {
continue;
}
const finalResult = test.results[test.results.length - 1];
const finalStatus = finalResult.status;
// Check if any result was a failure
const hadFailure = test.results.some(
(r) => r.status === "failed" || r.status === "timedOut",
);
if (finalStatus === "skipped") {
skipped++;
} else if (finalStatus === "failed" || finalStatus === "timedOut") {
unexpected++;
} else if (finalStatus === "passed") {
if (hadFailure) {
flaky++;
} else {
expected++;
}
}
}
// Compute duration as sum of both runs
const duration =
(originalStats?.duration || 0) + (retestStats?.duration || 0);
return {
startTime: originalStats?.startTime || new Date().toISOString(),
duration,
expected,
unexpected,
skipped,
flaky,
};
}
/**
* Format milliseconds as "Xm Ys"
*/
function formatDuration(ms: number): string {
const totalSeconds = Math.round(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}m ${seconds}s`;
}
/**
* Get color based on pass rate
*/
function getColor(passRate: number): string {
if (passRate === 100) {
return "#43A047"; // green
} else if (passRate >= 99) {
return "#FFEB3B"; // yellow
} else if (passRate >= 98) {
return "#FF9800"; // orange
} else {
return "#F44336"; // red
}
}
/**
* Calculate all outputs from results
*/
export function calculateResults(
results: PlaywrightResults,
): CalculationResult {
const stats = results.stats || {
expected: 0,
unexpected: 0,
skipped: 0,
flaky: 0,
startTime: new Date().toISOString(),
duration: 0,
};
const passed = stats.expected;
const failed = stats.unexpected;
const flaky = stats.flaky;
const skipped = stats.skipped;
// Count unique spec files
const specFiles = new Set<string>();
for (const suite of results.suites) {
specFiles.add(suite.file);
}
const totalSpecs = specFiles.size;
// Get all tests with info for failed tests extraction
const testsInfo = getAllTestsWithInfo(results.suites);
// Extract failed specs
const failedSpecsSet = new Set<string>();
const failedTestsList: FailedTest[] = [];
for (const test of testsInfo) {
if (test.finalStatus === "failed" || test.finalStatus === "timedOut") {
failedSpecsSet.add(test.file);
failedTestsList.push({
title: test.title,
file: test.file,
});
}
}
const failedSpecs = Array.from(failedSpecsSet).join(",");
const failedSpecsCount = failedSpecsSet.size;
// Build failed tests markdown table (limit to 10)
let failedTests = "";
const uniqueFailedTests = failedTestsList.filter(
(test, index, self) =>
index ===
self.findIndex(
(t) => t.title === test.title && t.file === test.file,
),
);
if (uniqueFailedTests.length > 0) {
const limitedTests = uniqueFailedTests.slice(0, 10);
failedTests = limitedTests
.map((t) => {
const escapedTitle = t.title
.replace(/`/g, "\\`")
.replace(/\|/g, "\\|");
return `| ${escapedTitle} | ${t.file} |`;
})
.join("\n");
if (uniqueFailedTests.length > 10) {
const remaining = uniqueFailedTests.length - 10;
failedTests += `\n| _...and ${remaining} more failed tests_ | |`;
}
} else if (failed > 0) {
failedTests = "| Unable to parse failed tests | - |";
}
// Calculate totals and pass rate
const passing = passed + flaky;
const total = passing + failed;
const passRate = total > 0 ? ((passing * 100) / total).toFixed(2) : "0.00";
const color = getColor(parseFloat(passRate));
// Build commit status message
const rate = total > 0 ? (passing * 100) / total : 0;
const rateStr = rate === 100 ? "100%" : `${rate.toFixed(1)}%`;
const specSuffix = totalSpecs > 0 ? `, ${totalSpecs} specs` : "";
const commitStatusMessage =
rate === 100
? `${rateStr} passed (${passing})${specSuffix}`
: `${rateStr} passed (${passing}/${total}), ${failed} failed${specSuffix}`;
const testDuration = formatDuration(stats.duration || 0);
return {
passed,
failed,
flaky,
skipped,
totalSpecs,
commitStatusMessage,
failedSpecs,
failedSpecsCount,
failedTests,
total,
passRate,
passing,
color,
testDuration,
};
}
/**
* Merge original and retest results at suite level
* - Keep original suites that are NOT in retest
* - Add all retest suites (replacing matching originals)
*/
export function mergeResults(
original: PlaywrightResults,
retest: PlaywrightResults,
): MergeResult {
// Get list of retested spec files
const retestFiles = retest.suites.map((s) => s.file);
// Filter original suites - keep only those NOT in retest
const keptOriginalSuites = original.suites.filter(
(suite) => !retestFiles.includes(suite.file),
);
// Merge: kept original suites + all retest suites
const mergedSuites = [...keptOriginalSuites, ...retest.suites];
// Compute stats from merged suites
const stats = computeStats(mergedSuites, original.stats, retest.stats);
const merged: PlaywrightResults = {
config: original.config,
suites: mergedSuites,
stats,
};
return {
merged,
stats,
totalSuites: mergedSuites.length,
retestFiles,
};
}

Просмотреть файл

@@ -1,89 +0,0 @@
export interface PlaywrightResults {
config: Record<string, unknown>;
suites: Suite[];
stats?: Stats;
}
export interface Suite {
title: string;
file: string;
column: number;
line: number;
specs: Spec[];
suites?: Suite[];
}
export interface Spec {
title: string;
ok: boolean;
tags: string[];
tests: Test[];
}
export interface Test {
timeout: number;
annotations: unknown[];
expectedStatus: string;
projectId: string;
projectName: string;
results: TestResult[];
location?: TestLocation;
}
export interface TestResult {
workerIndex: number;
parallelIndex: number;
status: string;
duration: number;
errors: unknown[];
stdout: unknown[];
stderr: unknown[];
retry: number;
startTime: string;
annotations: unknown[];
attachments?: unknown[];
}
export interface TestLocation {
file: string;
line: number;
column: number;
}
export interface Stats {
startTime: string;
duration: number;
expected: number;
unexpected: number;
skipped: number;
flaky: number;
}
export interface MergeResult {
merged: PlaywrightResults;
stats: Stats;
totalSuites: number;
retestFiles: string[];
}
export interface CalculationResult {
passed: number;
failed: number;
flaky: number;
skipped: number;
totalSpecs: number;
commitStatusMessage: string;
failedSpecs: string;
failedSpecsCount: number;
failedTests: string;
total: number;
passRate: string;
passing: number;
color: string;
testDuration: string;
}
export interface FailedTest {
title: string;
file: string;
}

Просмотреть файл

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "./src",
"declaration": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Просмотреть файл

@@ -1 +0,0 @@
{"root":["./src/index.ts","./src/main.ts","./src/merge.ts","./src/types.ts"],"version":"5.9.3"}

Просмотреть файл

@@ -1,12 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["cjs"],
outDir: "dist",
clean: true,
noExternal: [/.*/], // Bundle all dependencies
minify: false,
sourcemap: false,
target: "node24",
});

104
.github/actions/check-e2e-test-only/action.yml поставляемый
Просмотреть файл

@@ -1,104 +0,0 @@
---
name: Check E2E Test Only
description: Check if PR contains only E2E test changes and determine the appropriate docker image tag
inputs:
base_sha:
description: Base commit SHA (PR base)
required: false
head_sha:
description: Head commit SHA (PR head)
required: false
pr_number:
description: PR number (used to fetch SHAs via API if base_sha/head_sha not provided)
required: false
outputs:
e2e_test_only:
description: Whether the PR contains only E2E test changes (true/false)
value: ${{ steps.check.outputs.e2e_test_only }}
image_tag:
description: Docker image tag to use (base branch ref for E2E-only, short SHA for mixed)
value: ${{ steps.check.outputs.image_tag }}
runs:
using: composite
steps:
- name: ci/check-e2e-test-only
id: check
shell: bash
env:
GH_TOKEN: ${{ github.token }}
INPUT_BASE_SHA: ${{ inputs.base_sha }}
INPUT_HEAD_SHA: ${{ inputs.head_sha }}
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
run: |
# Resolve SHAs and base branch from PR number if not provided
BASE_REF=""
if [ -z "$INPUT_BASE_SHA" ] || [ -z "$INPUT_HEAD_SHA" ]; then
if [ -z "$INPUT_PR_NUMBER" ]; then
echo "::error::Either base_sha/head_sha or pr_number must be provided"
exit 1
fi
echo "Resolving SHAs from PR #${INPUT_PR_NUMBER}"
PR_DATA=$(gh api "repos/${{ github.repository }}/pulls/${INPUT_PR_NUMBER}")
INPUT_BASE_SHA=$(echo "$PR_DATA" | jq -r '.base.sha')
INPUT_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head.sha')
BASE_REF=$(echo "$PR_DATA" | jq -r '.base.ref')
if [ -z "$INPUT_BASE_SHA" ] || [ "$INPUT_BASE_SHA" = "null" ] || \
[ -z "$INPUT_HEAD_SHA" ] || [ "$INPUT_HEAD_SHA" = "null" ]; then
echo "::error::Could not resolve SHAs for PR #${INPUT_PR_NUMBER}"
exit 1
fi
elif [ -n "$INPUT_PR_NUMBER" ]; then
# SHAs provided but we still need the base branch ref
BASE_REF=$(gh api "repos/${{ github.repository }}/pulls/${INPUT_PR_NUMBER}" --jq '.base.ref')
fi
# Default to master if base ref could not be determined
if [ -z "$BASE_REF" ] || [ "$BASE_REF" = "null" ]; then
BASE_REF="master"
fi
echo "PR base branch: ${BASE_REF}"
SHORT_SHA="${INPUT_HEAD_SHA::7}"
# Get changed files - try git first, fall back to API
CHANGED_FILES=$(git diff --name-only "$INPUT_BASE_SHA"..."$INPUT_HEAD_SHA" 2>/dev/null || \
gh api "repos/${{ github.repository }}/pulls/${INPUT_PR_NUMBER}/files" --jq '.[].filename' 2>/dev/null || echo "")
if [ -z "$CHANGED_FILES" ]; then
echo "::warning::Could not determine changed files, assuming not E2E-only"
echo "e2e_test_only=false" >> $GITHUB_OUTPUT
echo "image_tag=${SHORT_SHA}" >> $GITHUB_OUTPUT
exit 0
fi
echo "Changed files:"
echo "$CHANGED_FILES"
# Check if all files are E2E-related
E2E_TEST_ONLY="true"
while IFS= read -r file; do
[ -z "$file" ] && continue
if [[ ! "$file" =~ ^e2e-tests/ ]] && \
[[ ! "$file" =~ ^\.github/workflows/e2e- ]] && \
[[ ! "$file" =~ ^\.github/actions/ ]]; then
echo "Non-E2E file found: $file"
E2E_TEST_ONLY="false"
break
fi
done <<< "$CHANGED_FILES"
echo "E2E test only: ${E2E_TEST_ONLY}"
# Set outputs
echo "e2e_test_only=${E2E_TEST_ONLY}" >> $GITHUB_OUTPUT
if [ "$E2E_TEST_ONLY" = "true" ] && \
{ [ "$BASE_REF" = "master" ] || [[ "$BASE_REF" =~ ^release-[0-9]+\.[0-9]+$ ]]; }; then
echo "image_tag=${BASE_REF}" >> $GITHUB_OUTPUT
else
echo "image_tag=${SHORT_SHA}" >> $GITHUB_OUTPUT
fi

17
.github/codecov.yml поставляемый
Просмотреть файл

@@ -1,17 +0,0 @@
comment:
layout: "condensed_header, condensed_files, condensed_footer"
behavior: default
require_changes: "uncovered_patch" # only post comment if the patch has uncovered lines
hide_project_coverage: true # only show coverage on the git diff
coverage:
status:
changes: false
patch: false
project:
default:
threshold: 1.0
codecov:
notify:
after_n_builds: 2 # Server and webapp at this point
ignore:
- ^store/storetest.*

7
.github/workflows/api.yml поставляемый
Просмотреть файл

@@ -20,7 +20,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: .nvmrc
cache: "npm"
@@ -28,3 +28,8 @@ jobs:
- name: Run build
run: make build
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: mattermost-api-reference
path: api/v4/html

Просмотреть файл

@@ -16,13 +16,13 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: opensearch/docker-login
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
- name: opensearch/build-and-push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
with:
provenance: false
file: server/build/Dockerfile.opensearch

4
.github/workflows/build-server-image.yml поставляемый
Просмотреть файл

@@ -21,13 +21,13 @@ jobs:
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: buildenv/docker-login
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
- name: buildenv/build-and-push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
with:
provenance: false
file: server/build/Dockerfile.buildenv

38
.github/workflows/claude.yml поставляемый
Просмотреть файл

@@ -1,38 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
model: claude-sonnet-4-20250514
allowed_tools: "Bash(cd:*),Bash(git:*),Bash(make:*),Bash(npm:*),Bash(go:*)"

61
.github/workflows/codeql-analysis.yml поставляемый
Просмотреть файл

@@ -3,9 +3,9 @@ name: "CodeQL"
on:
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
branches: [ master ]
schedule:
- cron: "30 5,17 * * *"
- cron: '30 5,17 * * *'
permissions:
contents: read
@@ -13,7 +13,7 @@ permissions:
jobs:
analyze:
permissions:
security-events: write # for github/codeql-action/autobuild to send a status report
security-events: write # for github/codeql-action/autobuild to send a status report
name: Analyze
if: github.repository_owner == 'mattermost'
runs-on: ubuntu-latest
@@ -21,37 +21,38 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ["go", "javascript"]
language: [ 'go', 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
languages: ${{ matrix.language }}
debug: false
config-file: ./.github/codeql/codeql-config.yml
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.9
with:
languages: ${{ matrix.language }}
debug: false
config-file: ./.github/codeql/codeql-config.yml
- name: Build JavaScript
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
if: ${{ matrix.language == 'javascript' }}
- name: Build JavaScript
uses: github/codeql-action/autobuild@v3.28.9
if: ${{ matrix.language == 'javascript' }}
- name: Setup go
uses: actions/setup-go@v5
with:
go-version-file: server/go.mod
if: ${{ matrix.language == 'go' }}
- name: Setup go
uses: actions/setup-go@v5
with:
go-version: '1.22'
if: ${{ matrix.language == 'go' }}
- name: Build Golang
run: |
cd server
make setup-go-work
make build-linux-amd64
if: ${{ matrix.language == 'go' }}
# Perform Analysis
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
- name: Build Golang
run: |
cd server
make setup-go-work
make build-linux-amd64
if: ${{ matrix.language == 'go' }}
# Perform Analysis
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.9

Просмотреть файл

@@ -1,56 +0,0 @@
# .github/workflows/dispatch-build.yml
name: Build & Push New Golang Docker Build Server Image
on:
workflow_dispatch:
inputs:
branch:
description: 'Git branch or PR ref to build'
required: true
tag:
description: 'Docker image tag (e.g. v1.2.3 or latest)'
required: true
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
with:
ref: ${{ github.event.inputs.branch }}
- name: Set up QEMU (optional, for multi-arch)
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca
- name: Login to DockerHub (development repo)
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
- name: Build & push development image
run: |
docker buildx build \
--tag mattermostdevelopment/mattermost-build-server:${{ github.event.inputs.tag }} \
--push \
-f Dockerfile.buildenv .
- name: Login to DockerHub (production repo)
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build & push production image
run: |
docker buildx build \
--tag mattermost/mattermost-build-server:${{ github.event.inputs.tag }} \
--push \
-f Dockerfile.buildenv .

2
.github/workflows/docker-push-mirrored.yml поставляемый
Просмотреть файл

@@ -16,7 +16,7 @@ jobs:
- name: Checkout mattermost project
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: cd/Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}

337
.github/workflows/e2e-fulltests-ci.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,337 @@
---
name: E2E Tests
on:
# For PRs, this workflow gets triggered from the Argo Events platform.
# Check the following repo for details: https://github.com/mattermost/delivery-platform
workflow_dispatch:
inputs:
ref:
type: string
description: Git ref to test. Must be a full commit SHA for PR testing, and a tag for release testing. Ignored for daily tests.
required: false
PR_NUMBER:
type: string
description: PR number (if applicable)
required: false
ROLLING_RELEASE_FROM_TAG:
type: string
description: Mattermost release git tag for RollingRelease tests. Optional.
required: false
MM_ENV:
type: string
required: false
description: A comma-separated list of environment variables to set for the server. Spaces are not supported.
MM_SERVICE_OVERRIDES:
type: string
required: false
description: A comma-separated list of service overrides. E.g. "-elasticsearch,+opensearch"
REPORT_TYPE:
type: choice
description: The context this report is being generated in
options:
- PR
- RELEASE
- RELEASE_CLOUD
- MASTER
- MASTER_UNSTABLE
- CLOUD
- CLOUD_UNSTABLE
- NONE
default: NONE
RUN_CYPRESS:
type: string
description: Enable Cypress run
default: "true"
RUN_PLAYWRIGHT:
type: string
description: Enable Playwright run
default: "true"
concurrency:
group: "${{ github.workflow }}-${{ inputs.REPORT_TYPE }}-${{ inputs.PR_NUMBER || inputs.ref }}-${{ inputs.MM_ENV }}"
cancel-in-progress: true
jobs:
generate-test-variables:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
defaults:
run:
shell: bash
outputs:
commit_sha: "${{ steps.generate.outputs.commit_sha }}"
BRANCH: "${{ steps.generate.outputs.BRANCH }}"
SERVER_IMAGE: "${{ steps.generate.outputs.SERVER_IMAGE }}"
status_check_context: "${{ steps.generate.outputs.status_check_context }}"
workers_number: "${{ steps.generate.outputs.workers_number }}"
server_uppercase: "${{ steps.generate.outputs.server_uppercase }}" # Required for license selection
SERVER: "${{ steps.generate.outputs.SERVER }}"
ENABLED_DOCKER_SERVICES: "${{ steps.generate.outputs.ENABLED_DOCKER_SERVICES }}"
TEST_FILTER_CYPRESS: "${{ steps.generate.outputs.TEST_FILTER_CYPRESS }}"
TEST_FILTER_PLAYWRIGHT: "tests/ --project=chrome" # Note: Run on chrome but eventually will enable to all projects which include firefox and ipad.
BUILD_ID: "${{ steps.generate.outputs.BUILD_ID }}"
TM4J_ENABLE: "${{ steps.generate.outputs.TM4J_ENABLE }}"
REPORT_TYPE: "${{ steps.generate.outputs.REPORT_TYPE }}"
TESTCASE_FAILURE_FATAL: "${{ steps.generate.outputs.TESTCASE_FAILURE_FATAL }}"
ROLLING_RELEASE_commit_sha: "${{ steps.generate.outputs.ROLLING_RELEASE_commit_sha }}"
ROLLING_RELEASE_SERVER_IMAGE: "${{ steps.generate.outputs.ROLLING_RELEASE_SERVER_IMAGE }}"
WORKFLOW_RUN_URL: "${{steps.generate.outputs.WORKFLOW_RUN_URL}}"
CYCLE_URL: "${{steps.generate.outputs.CYCLE_URL}}"
env:
GH_TOKEN: "${{ github.token }}"
REF: "${{ inputs.ref || github.sha }}"
PR_NUMBER: "${{ inputs.PR_NUMBER || '' }}"
REPORT_TYPE: "${{ inputs.REPORT_TYPE }}"
ROLLING_RELEASE_FROM_TAG: "${{ inputs.ROLLING_RELEASE_FROM_TAG }}"
AUTOMATION_DASHBOARD_URL: "${{ secrets.MM_E2E_AUTOMATION_DASHBOARD_URL }}"
# We could exclude the @smoke group for PRs, but then we wouldn't have it in the report
TEST_FILTER_CYPRESS_PR: >-
--stage="@prod"
--excludeGroup="@te_only,@cloud_only,@high_availability"
--sortFirst="@compliance_export,@elasticsearch,@ldap_group,@ldap"
--sortLast="@saml,@keycloak,@plugin,@plugins_uninstall,@mfa,@license_removal"
TEST_FILTER_CYPRESS_PROD_ONPREM: >-
--stage="@prod"
--excludeGroup="@te_only,@cloud_only,@high_availability"
--sortFirst="@compliance_export,@elasticsearch,@ldap_group,@ldap,@playbooks"
--sortLast="@saml,@keycloak,@plugin,@plugins_uninstall,@mfa,@license_removal"
TEST_FILTER_CYPRESS_PROD_CLOUD: >-
--stage="@prod"
--excludeGroup="@not_cloud,@cloud_trial,@e20_only,@te_only,@high_availability,@license_removal"
--sortFirst="@compliance_export,@elasticsearch,@ldap_group,@ldap,@playbooks"
--sortLast="@saml,@keycloak,@plugin,@plugins_uninstall,@mfa"
MM_ENV: "${{ inputs.MM_ENV || '' }}"
MM_SERVICE_OVERRIDES: "${{ inputs.MM_SERVICE_OVERRIDES }}"
steps:
- name: ci/checkout-repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: "${{ inputs.ref || github.sha }}"
fetch-depth: 0
- name: ci/generate-test-variables
id: generate
run: |
MM_ENV_HASH=$(md5sum -z <<<"$MM_ENV" | cut -c-8)
TESTCASE_FAILURE_FATAL="true"
if grep -q CLOUD <<<"$REPORT_TYPE"; then
SERVER=cloud
else
SERVER=onprem
fi
case "$REPORT_TYPE" in
NONE | PR)
### Populate support variables
_COMMIT_SHA_COMPUTED=$(git rev-parse --verify "$REF") # NB: not actually used for resolving the commit; it's only to double check the value of 'inputs.ref'
### For image tag generation: utilize 'inputs.ref', assume that it is a full commit SHA
COMMIT_SHA="${REF}"
BRANCH="server-pr-${PR_NUMBER}" # For reference, the real branch name may be retrievable with command: 'jq -r .head.ref <pr.json'
SERVER_IMAGE_TAG="${COMMIT_SHA::7}"
SERVER_IMAGE_ORG=mattermostdevelopment
BUILD_ID_SUFFIX="${REPORT_TYPE@L}-${SERVER}-ent"
WORKERS_NUMBER=20
TEST_FILTER_CYPRESS="$TEST_FILTER_CYPRESS_PR"
COMPUTED_REPORT_TYPE="${REPORT_TYPE}"
### Run sanity assertions after variable generations
[ "$REF" = "${_COMMIT_SHA_COMPUTED}" ] # 'inputs.ref' must be a full commit hash, and the commit must exist
[ "$REPORT_TYPE" != "PR" ] || [ "$PR_NUMBER" -gt "0" ] # If report type is PR, then PR_NUMBER must be set to a number
;;
MASTER | MASTER_UNSTABLE | CLOUD | CLOUD_UNSTABLE)
### Populate support variables
_IS_TEST_UNSTABLE=$(sed -n -E 's/^.*(UNSTABLE).*$/\1/p' <<< "$REPORT_TYPE") # The variable's value is 'UNSTABLE' if report type is for unstable tests, otherwise it's empty
_TEST_FILTER_CYPRESS_VARIABLE="TEST_FILTER_CYPRESS_PROD_${SERVER@U}"
### For ref and image tag generation: ignore 'inputs.ref', and use master branch directly. Note that 'COMMIT_SHA' will be used for reporting the test result, and for checking out the testing scripts and test cases
COMMIT_SHA="$(git rev-parse --verify origin/master)"
BRANCH=master
SERVER_IMAGE_TAG=master
SERVER_IMAGE_ORG=mattermostdevelopment
BUILD_ID_SUFFIX="${_IS_TEST_UNSTABLE:+unstable-}daily-${SERVER}-ent"
BUILD_ID_SUFFIX_IN_STATUS_CHECK=true
WORKERS_NUMBER=10 # Daily tests are not time critical, and it's more efficient to run on fewer workers
TEST_FILTER_CYPRESS="${!_TEST_FILTER_CYPRESS_VARIABLE} ${_IS_TEST_UNSTABLE:+--invert}"
TM4J_ENABLE=true
COMPUTED_REPORT_TYPE="${REPORT_TYPE}"
[ -z "$_IS_TEST_UNSTABLE" ] || TESTCASE_FAILURE_FATAL="" # Assert that tests are stable. If they are not, the status check will be always green
;;
RELEASE | RELEASE_CLOUD)
### Populate support variables
_TEST_FILTER_CYPRESS_VARIABLE="TEST_FILTER_CYPRESS_PROD_${SERVER@U}"
### For ref and image tag generation: assume the 'inputs.ref' is a tag, and use the first two digits to construct the branch name
COMMIT_SHA="$(git rev-parse --verify HEAD)"
BRANCH=$(sed -E "s/v([0-9]+)\.([0-9]+)\..+$/release-\1.\2/g" <<<$REF)
SERVER_IMAGE_TAG="$(cut -c2- <<<$REF)" # Remove the leading 'v' from the given tag name, to generate the docker image tag
SERVER_IMAGE_ORG=mattermost
BUILD_ID_SUFFIX="release-${SERVER}-ent"
BUILD_ID_SUFFIX_IN_STATUS_CHECK=true
WORKERS_NUMBER=20
TEST_FILTER_CYPRESS="${!_TEST_FILTER_CYPRESS_VARIABLE}"
TM4J_ENABLE=true
COMPUTED_REPORT_TYPE=RELEASE
### Run sanity assertions after variable generations
git show-ref --verify "refs/tags/${REF}" # 'inputs.ref' must be a tag, for release report types
git show-ref --verify "refs/remotes/origin/${BRANCH}" # The release branch computed from the given tag must exist
;;
*)
echo "Fatal: unimplemented test type. Aborting."
exit 1
esac
if [ -n "$ROLLING_RELEASE_FROM_TAG" ]; then
ROLLING_RELEASE_COMMIT_SHA=$(git rev-parse --verify "$ROLLING_RELEASE_FROM_TAG")
ROLLING_RELEASE_SERVER_IMAGE_TAG=$(echo "$ROLLING_RELEASE_FROM_TAG" | sed 's/^v//') # Remove the leading 'v' from the given tag name, to generate the docker image tag
ROLLING_RELEASE_SERVER_IMAGE="mattermost/mattermost-enterprise-edition:${ROLLING_RELEASE_SERVER_IMAGE_TAG}"
BUILD_ID_SUFFIX="rolling${ROLLING_RELEASE_FROM_TAG/-/_}-$BUILD_ID_SUFFIX"
BUILD_ID_SUFFIX_IN_STATUS_CHECK=true
WORKERS_NUMBER=10 # Rolling release tests are particularly impacted by increased parallelism. It's more efficient to run on fewer workers
### Run sanity assertions after variable generations
git show-ref --verify "refs/tags/${ROLLING_RELEASE_FROM_TAG}" # 'inputs.ROLLING_RELEASE_FROM_TAG' must be a tag, for release report types
fi
ENABLED_DOCKER_SERVICES="postgres inbucket minio openldap elasticsearch keycloak"
for SVC_OP in $(tr , ' '<<<"$MM_SERVICE_OVERRIDES"); do
OP=$(cut -c1 <<<$SVC_OP)
SVC=$(cut -c2- <<<$SVC_OP)
case "$OP" in
"+") ENABLED_DOCKER_SERVICES="$ENABLED_DOCKER_SERVICES $SVC" ;;
"-") ENABLED_DOCKER_SERVICES=$(sed -E "s:(^| )${SVC}( |\$): :g" <<<"$ENABLED_DOCKER_SERVICES") ;;
*) echo "Invalid MM_SERVICE_OVERRIDE value: $SVC_OP"; exit 1 ;;
esac
done
# BUILD_ID format: $pipelineID-$imageTag-$testType-$serverType-$serverEdition
# Reference on BUILD_ID parsing: https://github.com/saturninoabril/automation-dashboard/blob/175891781bf1072c162c58c6ec0abfc5bcb3520e/lib/common_utils.ts#L3-L23
BUILD_ID="${{ github.run_id }}_${{ github.run_attempt }}-${SERVER_IMAGE_TAG}-${BUILD_ID_SUFFIX}"
echo "commit_sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT
echo "BRANCH=${BRANCH}" >> $GITHUB_OUTPUT
echo "SERVER_IMAGE=${SERVER_IMAGE_ORG}/mattermost-enterprise-edition:${SERVER_IMAGE_TAG}" >> $GITHUB_OUTPUT
echo "SERVER=${SERVER}" >> $GITHUB_OUTPUT
echo "server_uppercase=${SERVER@U}" >> $GITHUB_OUTPUT
echo "ENABLED_DOCKER_SERVICES=${ENABLED_DOCKER_SERVICES}" >> $GITHUB_OUTPUT
echo "status_check_context=E2E Tests/test${BUILD_ID_SUFFIX_IN_STATUS_CHECK:+-$BUILD_ID_SUFFIX}${MM_ENV:+/$MM_ENV_HASH}" >> $GITHUB_OUTPUT
echo "workers_number=${WORKERS_NUMBER}" >> $GITHUB_OUTPUT
echo "TEST_FILTER_CYPRESS=${TEST_FILTER_CYPRESS}" >> $GITHUB_OUTPUT
echo "TESTCASE_FAILURE_FATAL=${TESTCASE_FAILURE_FATAL}" >> $GITHUB_OUTPUT
echo "TM4J_ENABLE=${TM4J_ENABLE:-}" >> $GITHUB_OUTPUT
echo "REPORT_TYPE=${COMPUTED_REPORT_TYPE}" >> $GITHUB_OUTPUT
echo "ROLLING_RELEASE_commit_sha=${ROLLING_RELEASE_COMMIT_SHA}" >> $GITHUB_OUTPUT
echo "ROLLING_RELEASE_SERVER_IMAGE=${ROLLING_RELEASE_SERVER_IMAGE}" >> $GITHUB_OUTPUT
echo "BUILD_ID=${BUILD_ID}" >> $GITHUB_OUTPUT
# User notification variables
echo "WORKFLOW_RUN_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{github.run_id}}" >> $GITHUB_OUTPUT
echo "CYCLE_URL=${AUTOMATION_DASHBOARD_URL%%/api}/cycle/${BUILD_ID}" >> $GITHUB_OUTPUT
- name: ci/notify-user
env:
COMMIT_SHA: "${{steps.generate.outputs.commit_sha}}"
STATUS_CHECK_CONTEXT: "${{steps.generate.outputs.status_check_context}}"
WORKFLOW_RUN_URL: "${{steps.generate.outputs.WORKFLOW_RUN_URL}}"
CYCLE_URL: "${{steps.generate.outputs.CYCLE_URL}}"
RUN_CYPRESS: "${{inputs.RUN_CYPRESS == 'true' || ''}}"
RUN_PLAYWRIGHT: "${{inputs.RUN_PLAYWRIGHT == 'true' || ''}}"
run: |
if [ -n "$PR_NUMBER" ]; then
gh issue -R "${{ github.repository }}" comment "$PR_NUMBER" --body-file - <<EOF
E2E test run is starting for commit \`${COMMIT_SHA}\`${MM_ENV:+, with \`MM_ENV=$MM_ENV\`}${MM_SERVICE_OVERRIDES:+, Cypress service overrides \`$MM_SERVICE_OVERRIDES\`}.
To check the run progress:
- Cypress: ${RUN_CYPRESS:+look for commit status \`$STATUS_CHECK_CONTEXT\` or the access the [Automation Dashboard Cycle URL]($CYCLE_URL)}$([ -n "${RUN_CYPRESS:-}" ] || echo -n "will not run").
- Playwright: ${RUN_PLAYWRIGHT:+look for commit status \`$STATUS_CHECK_CONTEXT-playwright\`}$([ -n "${RUN_PLAYWRIGHT:-}" ] || echo -n "will not run").
You can also look at the [E2E test's Workflow Run URL]($WORKFLOW_RUN_URL) (run ID \`${{ github.run_id }}\`).
EOF
fi
e2e-fulltest-cypress:
needs:
- generate-test-variables
uses: ./.github/workflows/e2e-tests-ci-template.yml
if: ${{ inputs.RUN_CYPRESS == 'true' }}
with:
commit_sha: "${{ needs.generate-test-variables.outputs.commit_sha }}"
status_check_context: "${{ needs.generate-test-variables.outputs.status_check_context }}"
workers_number: "${{ needs.generate-test-variables.outputs.workers_number }}"
testcase_failure_fatal: "${{ needs.generate-test-variables.outputs.TESTCASE_FAILURE_FATAL == 'true' }}"
run_preflight_checks: false
enable_reporting: true
SERVER: "${{ needs.generate-test-variables.outputs.SERVER }}"
SERVER_IMAGE: "${{ needs.generate-test-variables.outputs.SERVER_IMAGE }}"
ENABLED_DOCKER_SERVICES: "${{ needs.generate-test-variables.outputs.ENABLED_DOCKER_SERVICES }}"
TEST_FILTER: "${{ needs.generate-test-variables.outputs.TEST_FILTER_CYPRESS }}"
MM_ENV: "${{ inputs.MM_ENV || '' }}"
BRANCH: "${{ needs.generate-test-variables.outputs.BRANCH }}"
BUILD_ID: "${{ needs.generate-test-variables.outputs.BUILD_ID }}"
REPORT_TYPE: "${{ needs.generate-test-variables.outputs.REPORT_TYPE }}"
ROLLING_RELEASE_commit_sha: "${{ needs.generate-test-variables.outputs.ROLLING_RELEASE_commit_sha }}"
ROLLING_RELEASE_SERVER_IMAGE: "${{ needs.generate-test-variables.outputs.ROLLING_RELEASE_SERVER_IMAGE }}"
secrets:
MM_LICENSE: "${{ secrets[format('MM_E2E_TEST_LICENSE_{0}_ENT', needs.generate-test-variables.outputs.server_uppercase)] }}"
AUTOMATION_DASHBOARD_URL: "${{ secrets.MM_E2E_AUTOMATION_DASHBOARD_URL }}"
AUTOMATION_DASHBOARD_TOKEN: "${{ secrets.MM_E2E_AUTOMATION_DASHBOARD_TOKEN }}"
PUSH_NOTIFICATION_SERVER: "${{ secrets.MM_E2E_PUSH_NOTIFICATION_SERVER }}"
REPORT_WEBHOOK_URL: "${{ secrets.MM_E2E_REPORT_WEBHOOK_URL }}"
REPORT_TM4J_API_KEY: "${{ needs.generate-test-variables.outputs.TM4J_ENABLE == 'true' && secrets.MM_E2E_TM4J_API_KEY || '' }}"
REPORT_TM4J_TEST_CYCLE_LINK_PREFIX: "${{ secrets.MM_E2E_TEST_CYCLE_LINK_PREFIX }}"
CWS_URL: "${{ needs.generate-test-variables.outputs.SERVER == 'cloud' && secrets.MM_E2E_CWS_URL || '' }}"
CWS_EXTRA_HTTP_HEADERS: "${{ needs.generate-test-variables.outputs.SERVER == 'cloud' && secrets.MM_E2E_CWS_EXTRA_HTTP_HEADERS || '' }}"
e2e-fulltest-playwright:
needs:
- generate-test-variables
uses: ./.github/workflows/e2e-tests-ci-template.yml
if: ${{ inputs.RUN_PLAYWRIGHT == 'true' }}
with:
commit_sha: "${{ needs.generate-test-variables.outputs.commit_sha }}"
status_check_context: "${{ needs.generate-test-variables.outputs.status_check_context }}-playwright"
workers_number: "1"
testcase_failure_fatal: "${{ needs.generate-test-variables.outputs.TESTCASE_FAILURE_FATAL == 'true' }}"
run_preflight_checks: false
enable_reporting: true
SERVER: "${{ needs.generate-test-variables.outputs.SERVER }}"
SERVER_IMAGE: "${{ needs.generate-test-variables.outputs.SERVER_IMAGE }}"
TEST: playwright
TEST_FILTER: "${{ needs.generate-test-variables.outputs.TEST_FILTER_PLAYWRIGHT }}"
MM_ENV: "${{ inputs.MM_ENV || '' }}"
BRANCH: "${{ needs.generate-test-variables.outputs.BRANCH }}"
BUILD_ID: "${{ needs.generate-test-variables.outputs.BUILD_ID }}"
REPORT_TYPE: "${{ needs.generate-test-variables.outputs.REPORT_TYPE }}"
ROLLING_RELEASE_commit_sha: "${{ needs.generate-test-variables.outputs.ROLLING_RELEASE_commit_sha }}"
ROLLING_RELEASE_SERVER_IMAGE: "${{ needs.generate-test-variables.outputs.ROLLING_RELEASE_SERVER_IMAGE }}"
secrets:
MM_LICENSE: "${{ secrets[format('MM_E2E_TEST_LICENSE_{0}_ENT', needs.generate-test-variables.outputs.server_uppercase)] }}"
PUSH_NOTIFICATION_SERVER: "${{ secrets.MM_E2E_PUSH_NOTIFICATION_SERVER }}"
REPORT_WEBHOOK_URL: "${{ secrets.MM_E2E_REPORT_WEBHOOK_URL }}"
CWS_URL: "${{ needs.generate-test-variables.outputs.SERVER == 'cloud' && secrets.MM_E2E_CWS_URL || '' }}"
CWS_EXTRA_HTTP_HEADERS: "${{ needs.generate-test-variables.outputs.SERVER == 'cloud' && secrets.MM_E2E_CWS_EXTRA_HTTP_HEADERS || '' }}"
notify-user:
runs-on: ubuntu-latest
if: always()
needs:
- generate-test-variables
- e2e-fulltest-cypress
- e2e-fulltest-playwright
permissions:
issues: write
pull-requests: write
defaults:
run:
shell: bash
env:
GH_TOKEN: "${{ github.token }}"
PR_NUMBER: "${{ inputs.PR_NUMBER || '' }}"
MM_ENV: "${{ inputs.MM_ENV || '' }}"
COMMIT_SHA: "${{ needs.generate-test-variables.outputs.commit_sha }}"
STATUS_CHECK_CONTEXT: "${{ needs.generate-test-variables.outputs.status_check_context }}"
WORKFLOW_RUN_URL: "${{ needs.generate-test-variables.outputs.WORKFLOW_RUN_URL }}"
CYCLE_URL: "${{ needs.generate-test-variables.outputs.CYCLE_URL }}"
RUN_CYPRESS: "${{inputs.RUN_CYPRESS == 'true' || ''}}"
RUN_PLAYWRIGHT: "${{inputs.RUN_PLAYWRIGHT == 'true' || ''}}"
steps:
- name: ci/notify-user-test-completion
run: |
if [ -n "$PR_NUMBER" ]; then
gh issue -R "${{ github.repository }}" comment "$PR_NUMBER" --body-file - <<EOF
E2E test has completed for commit \`${COMMIT_SHA}\`${MM_ENV:+, with \`MM_ENV=$MM_ENV\`}.
Results summary:
- Cypress: ${RUN_CYPRESS:+pass rate is \`${{ needs.e2e-fulltest-cypress.outputs.pass_rate || 'unknown' }}\` (see [Automation Dashboard]($CYCLE_URL) and commit status check \`$STATUS_CHECK_CONTEXT\`)}$([ -n "${RUN_CYPRESS:-}" ] || echo -n "did not run").
- Playwright: ${RUN_PLAYWRIGHT:+pass rate is \`${{ needs.e2e-fulltest-playwright.outputs.pass_rate || 'unknown' }}\` (see commit status check \`$STATUS_CHECK_CONTEXT-playwright\`)}$([ -n "${RUN_PLAYWRIGHT:-}" ] || echo -n "did not run").
The run summary artifacts are available in the corresponding [Workflow Run]($WORKFLOW_RUN_URL).
EOF
fi

510
.github/workflows/e2e-tests-ci-template.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,510 @@
---
name: E2E Tests Template
on:
workflow_call:
inputs:
# NB: this does not support using branch names that belong to forks.
# In those cases, you should specify directly the commit SHA that you want to test, or
# some wrapper workflow that does it for you (e.g. the slash command for initiating a PR test)
commit_sha:
type: string
required: true
status_check_context:
type: string
required: true
workers_number:
type: string # Should ideally be a number; see https://github.com/orgs/community/discussions/67182
required: false
default: "1"
testcase_failure_fatal:
type: boolean
required: false
default: true
# NB: the following toggles will skip individual steps, rather than the whole jobs,
# to let the dependent jobs run even if these are false
run_preflight_checks:
type: boolean
required: false
default: true
enable_reporting:
type: boolean
required: false
default: false
SERVER:
type: string # Valid values are: onprem, cloud
required: false
default: onprem
SERVER_IMAGE:
type: string
required: false
ENABLED_DOCKER_SERVICES:
type: string
required: false
TEST: # Valid values are: cypress, playwright
type: string
required: false
default: "cypress"
TEST_FILTER:
type: string
required: false
MM_ENV:
type: string
required: false
BRANCH:
type: string
required: false
BUILD_ID:
type: string
required: false
REPORT_TYPE:
type: string
required: false
ROLLING_RELEASE_commit_sha:
type: string
required: false
ROLLING_RELEASE_SERVER_IMAGE:
type: string
required: false
secrets:
MM_LICENSE:
required: false
AUTOMATION_DASHBOARD_URL:
required: false
AUTOMATION_DASHBOARD_TOKEN:
required: false
PUSH_NOTIFICATION_SERVER:
required: false
REPORT_WEBHOOK_URL:
required: false
REPORT_TM4J_API_KEY:
required: false
REPORT_TM4J_TEST_CYCLE_LINK_PREFIX:
required: false
CWS_URL:
required: false
CWS_EXTRA_HTTP_HEADERS:
required: false
outputs:
passed:
value: "${{ jobs.report.outputs.passed }}"
failed:
value: "${{ jobs.report.outputs.failed }}"
failed_expected:
value: "${{ jobs.report.outputs.failed_expected }}"
pass_rate:
value: "${{ jobs.report.outputs.pass_rate }}"
jobs:
update-initial-status:
runs-on: ubuntu-latest
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.status_check_context }}
description: E2E tests for mattermost server app
status: pending
cypress-check:
runs-on: ubuntu-latest
needs:
- update-initial-status
defaults:
run:
working-directory: e2e-tests/cypress
steps:
- name: ci/checkout-repo
if: "${{ inputs.run_preflight_checks }}"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-node
if: "${{ inputs.run_preflight_checks }}"
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
id: setup_node
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/cypress/package-lock.json"
- name: ci/cypress/npm-install
if: "${{ inputs.run_preflight_checks }}"
run: |
npm ci
- name: ci/cypress/npm-check
if: "${{ inputs.run_preflight_checks }}"
run: |
npm run check
playwright-check:
runs-on: ubuntu-latest
needs:
- update-initial-status
defaults:
run:
working-directory: e2e-tests/playwright
steps:
- name: ci/checkout-repo
if: "${{ inputs.run_preflight_checks }}"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-node
if: "${{ inputs.run_preflight_checks }}"
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
id: setup_node
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/playwright/package-lock.json"
- name: ci/get-webapp-node-modules
if: "${{ inputs.run_preflight_checks }}"
working-directory: webapp
# requires build of client and types
run: |
make node_modules
- name: ci/playwright/npm-install
if: "${{ inputs.run_preflight_checks }}"
run: |
npm ci
- name: ci/playwright/npm-check
if: "${{ inputs.run_preflight_checks }}"
run: |
npm run check
shell-check:
runs-on: ubuntu-latest
needs:
- update-initial-status
defaults:
run:
working-directory: e2e-tests
steps:
- name: ci/checkout-repo
if: "${{ inputs.run_preflight_checks }}"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/shell-check
if: "${{ inputs.run_preflight_checks }}"
run: make check-shell
generate-build-variables:
runs-on: ubuntu-latest
needs:
- update-initial-status
defaults:
run:
shell: bash
outputs:
workers: "${{ steps.generate.outputs.workers }}"
node-cache-dependency-path: "${{ steps.generate.outputs.node-cache-dependency-path }}"
steps:
- name: ci/checkout-repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/generate-build-variables
id: generate
env:
WORKERS: ${{ inputs.workers_number }}
TEST: ${{ inputs.TEST }}
run: |
[ "$WORKERS" -gt "0" ] # Assert that the workers number is an integer greater than 0
echo "workers="$(jq --slurp --compact-output '[range('"$WORKERS"')] | map(tostring)' /dev/null) >> $GITHUB_OUTPUT
echo "node-cache-dependency-path=e2e-tests/${TEST}/package-lock.json" >> $GITHUB_OUTPUT
generate-test-cycle:
runs-on: ubuntu-latest
needs:
- generate-build-variables
defaults:
run:
shell: bash
working-directory: e2e-tests
outputs:
status_check_url: "${{ steps.e2e-test-gencycle.outputs.status_check_url }}"
steps:
- name: ci/checkout-repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
id: setup_node
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/cypress/package-lock.json" # NB: the generate-cycle script is cypress-specific operation for now
- name: ci/e2e-test-gencycle
id: e2e-test-gencycle
env:
AUTOMATION_DASHBOARD_URL: "${{ secrets.AUTOMATION_DASHBOARD_URL }}"
AUTOMATION_DASHBOARD_TOKEN: "${{ secrets.AUTOMATION_DASHBOARD_TOKEN }}"
BRANCH: "${{ inputs.BRANCH }}"
BUILD_ID: "${{ inputs.BUILD_ID }}"
TEST: "${{ inputs.TEST }}"
TEST_FILTER: "${{ inputs.TEST_FILTER }}"
run: |
set -e -o pipefail
make generate-test-cycle | tee generate-test-cycle.out
# Extract cycle's dashboard URL, if present
TEST_CYCLE_ID=$(sed -nE "s/^.*id: '([^']+)'.*$/\1/p" <generate-test-cycle.out)
if [ -n "$TEST_CYCLE_ID" ]; then
echo "status_check_url=https://automation-dashboard.vercel.app/cycles/${TEST_CYCLE_ID}" >> $GITHUB_OUTPUT
else
echo "status_check_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
fi
test:
continue-on-error: true # Individual runner failures shouldn't prevent the completion of an E2E run
strategy:
fail-fast: false # Individual runner failures shouldn't prevent the completion of an E2E run
matrix:
#
# Note that E2E tests should be run only on ubuntu, for QA purposes.
# But it's useful to be able to run and debug the E2E tests for different OSes.
# Notes:
# - For MacOS: works on developer machines, but uses too many resources to be able to run on Github Actions
# - for Windows: cannot currently run on Github Actions, since the runners do not support running linux containers, at the moment
#
#os: [ubuntu-latest, windows-2022, macos-12-xl]
os: [ubuntu-latest]
worker_index: ${{ fromJSON(needs.generate-build-variables.outputs.workers) }} # https://docs.github.com/en/actions/learn-github-actions/expressions#example-returning-a-json-object
runs-on: "${{ matrix.os }}"
timeout-minutes: 120
needs:
- cypress-check
- playwright-check
- shell-check
- generate-build-variables
- generate-test-cycle
defaults:
run:
shell: bash
working-directory: e2e-tests
env:
AUTOMATION_DASHBOARD_URL: "${{ secrets.AUTOMATION_DASHBOARD_URL }}"
AUTOMATION_DASHBOARD_TOKEN: "${{ secrets.AUTOMATION_DASHBOARD_TOKEN }}"
SERVER: "${{ inputs.SERVER }}"
SERVER_IMAGE: "${{ inputs.SERVER_IMAGE }}"
MM_LICENSE: "${{ secrets.MM_LICENSE }}"
ENABLED_DOCKER_SERVICES: "${{ inputs.ENABLED_DOCKER_SERVICES }}"
TEST: "${{ inputs.TEST }}"
TEST_FILTER: "${{ inputs.TEST_FILTER }}"
MM_ENV: "${{ inputs.MM_ENV }}"
BRANCH: "${{ inputs.BRANCH }}"
BUILD_ID: "${{ inputs.BUILD_ID }}"
CI_BASE_URL: "${{ matrix.os }}-${{ matrix.worker_index }}"
CYPRESS_pushNotificationServer: "${{ secrets.PUSH_NOTIFICATION_SERVER }}"
CWS_URL: "${{ secrets.CWS_URL }}"
CWS_EXTRA_HTTP_HEADERS: "${{ secrets.CWS_EXTRA_HTTP_HEADERS }}"
ROLLING_RELEASE_COMMIT_SHA: "${{ inputs.ROLLING_RELEASE_commit_sha }}"
ROLLING_RELEASE_SERVER_IMAGE: "${{ inputs.ROLLING_RELEASE_SERVER_IMAGE }}"
steps:
- name: ci/checkout-repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-macos-docker
if: runner.os == 'macos'
# https://github.com/actions/runner-images/issues/17#issuecomment-1537238473
run: |
brew install docker docker-compose
colima start
mkdir -p ~/.docker/cli-plugins
ln -sfn /usr/local/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
- name: ci/setup-node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
id: setup_node
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: ${{ needs.generate-build-variables.outputs.node-cache-dependency-path }}
- name: ci/e2e-test
run: |
make cloud-init
if [ -n "$ROLLING_RELEASE_SERVER_IMAGE" ]; then
echo "RollingRelease: checking out E2E test cases from revision ${ROLLING_RELEASE_COMMIT_SHA}, for initial smoketest"
git checkout "${ROLLING_RELEASE_COMMIT_SHA}" -- "${TEST}/" && git status
(
echo "RollingRelease: running initial smoketest against image $ROLLING_RELEASE_SERVER_IMAGE"
export SERVER_IMAGE="$ROLLING_RELEASE_SERVER_IMAGE"
export TEST_FILTER=""
export AUTOMATION_DASHBOARD_URL=""
make
)
echo "RollingRelease: asserting smoketest result has zero failures."
FAILURES=$(jq -r '.failed' "${TEST}/results/summary.json")
if [ "$FAILURES" -ne "0" ]; then
echo "RollingRelease: initial smoketest for rolling release E2E run has nonzero ($FAILURES) failures. Aborting test run." >&2
exit 1
fi
rm -rfv "${TEST}/{results,logs}"
echo "RollingRelease: reset the E2E test cases to the revision to test"
git reset --hard HEAD && git status
echo "RollingRelease: smoketest completed. Starting full E2E tests."
fi
make
make cloud-teardown
- name: ci/e2e-test-store-results
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: always()
with:
name: e2e-test-results-${{ inputs.TEST }}-${{ matrix.os }}-${{ matrix.worker_index }}
path: |
e2e-tests/${{ inputs.TEST }}/logs/
e2e-tests/${{ inputs.TEST }}/results/
retention-days: 1
report:
runs-on: ubuntu-latest
needs:
- test
- generate-build-variables
defaults:
run:
shell: bash
working-directory: e2e-tests
outputs:
passed: "${{ steps.calculate-results.outputs.passed }}"
failed: "${{ steps.calculate-results.outputs.failed }}"
failed_expected: "${{ steps.calculate-results.outputs.failed_expected }}"
pass_rate: "${{ steps.calculate-results.outputs.pass_rate }}"
commit_status_message: "${{ steps.calculate-results.outputs.commit_status_message }}"
steps:
- name: ci/checkout-repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/download-artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
pattern: e2e-test-results-${{ inputs.TEST }}-*
path: e2e-tests/${{ inputs.TEST }}/
merge-multiple: true
- name: ci/upload-report-global
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: e2e-test-results-${{ inputs.TEST }}
path: |
e2e-tests/${{ inputs.TEST }}/logs/
e2e-tests/${{ inputs.TEST }}/results/
- name: ci/setup-node
if: "${{ inputs.enable_reporting }}"
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
id: setup_node
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: ${{ needs.generate-build-variables.outputs.node-cache-dependency-path }}
- name: ci/publish-report
if: "${{ inputs.enable_reporting }}"
env:
TYPE: "${{ inputs.REPORT_TYPE }}"
TEST: "${{ inputs.TEST }}"
SERVER: "${{ inputs.SERVER }}"
SERVER_IMAGE: "${{ inputs.SERVER_IMAGE }}"
AUTOMATION_DASHBOARD_URL: "${{ secrets.AUTOMATION_DASHBOARD_URL }}"
WEBHOOK_URL: "${{ secrets.REPORT_WEBHOOK_URL }}"
BRANCH: "${{ inputs.BRANCH }}"
BUILD_ID: "${{ inputs.BUILD_ID }}"
MM_ENV: "${{ inputs.MM_ENV }}"
TM4J_API_KEY: "${{ secrets.REPORT_TM4J_API_KEY }}"
TEST_CYCLE_LINK_PREFIX: "${{ secrets.REPORT_TM4J_TEST_CYCLE_LINK_PREFIX }}"
run: |
make report
# The results dir may have been modified as part of the reporting: re-upload
- name: ci/upload-report-global
if: "${{ inputs.enable_reporting }}"
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: e2e-test-results-${{ inputs.TEST }}
path: |
e2e-tests/${{ inputs.TEST }}/logs/
e2e-tests/${{ inputs.TEST }}/results/
overwrite: true
- name: ci/report-calculate-results
id: calculate-results
env:
TEST: "${{ inputs.TEST }}"
run: |
AD_CYCLE_FILE="${TEST}/results/ad_cycle.json"
if [ -f "$AD_CYCLE_FILE" ]; then
# Prefer using the Automation Dashboard's results to calculate failures
export PASSED=$(jq -r .pass "$AD_CYCLE_FILE")
export FAILED=$(jq -r .fail "$AD_CYCLE_FILE")
export FAILED_EXPECTED=$(jq -r ".known + .flaky + .skipped" "$AD_CYCLE_FILE")
else
# Otherwise, utilize summary.json to calculate the failures
# NB: in this job, this file only makes sense if a single worker is used, as with Playwright
export PASSED=$(jq '.passed' "${TEST}/results/summary.json")
export FAILED=$(jq '.failed' "${TEST}/results/summary.json")
export FAILED_EXPECTED=$(jq '.failed_expected' "${TEST}/results/summary.json")
fi
export TOTAL_SPECS=$(( PASSED + FAILED ))
export PASS_RATE=$(jq -r '100 * (env.PASSED | tonumber) / (env.TOTAL_SPECS | tonumber)' <<<'{}' | xargs -l printf '%.2f')
if [ "$FAILED" = "0" ]; then
export COMMIT_STATUS_MESSAGE="All test cases passed"
else
export COMMIT_STATUS_MESSAGE="${FAILED} test cases failed. Please check the workflow logs"
fi
echo "passed=${PASSED:?}" >> $GITHUB_OUTPUT
echo "failed=${FAILED:?}" >> $GITHUB_OUTPUT
echo "failed_expected=${FAILED_EXPECTED:?}" >> $GITHUB_OUTPUT
echo "pass_rate=${PASS_RATE:?}%" >> $GITHUB_OUTPUT
echo "commit_status_message=${COMMIT_STATUS_MESSAGE:?}" >> $GITHUB_OUTPUT
echo "$COMMIT_STATUS_MESSAGE"
- name: ci/e2e-test-assert-results
if: "${{ inputs.testcase_failure_fatal }}"
run: |
# Assert that the run contained 0 failures
[ "${{ steps.calculate-results.outputs.failed }}" = "0" ]
update-failure-final-status:
runs-on: ubuntu-latest
if: failure() || cancelled()
needs:
- generate-test-cycle
- test
- report
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.status_check_context }}
description: ${{ needs.report.outputs.commit_status_message || 'Error during test execution' }}
status: failure
target_url: "${{ needs.generate-test-cycle.outputs.status_check_url }}"
update-success-final-status:
runs-on: ubuntu-latest
if: success()
needs:
- generate-test-cycle
- test
- report
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.status_check_context }}
description: ${{ needs.report.outputs.commit_status_message || 'Error during test execution' }}
status: success
target_url: "${{ needs.generate-test-cycle.outputs.status_check_url }}"

224
.github/workflows/e2e-tests-ci.yml поставляемый
Просмотреть файл

@@ -1,225 +1,17 @@
---
name: E2E Tests (pull request)
name: E2E Smoketests
on:
# Argo Events Trigger (automated):
# - Triggered by: Enterprise CI/docker-image status check (success)
# - Payload: { ref: "<branch>", inputs: { commit_sha: "<sha>" } }
# - Uses commit-specific docker image
# - Checks for relevant file changes before running tests
#
# Manual Trigger:
# - Enter PR number only - commit SHA is resolved automatically from PR head
# - Uses commit-specific docker image
# - E2E tests always run (no file change check)
#
# For PRs, this workflow gets triggered from the Argo Events platform.
# Check the following repo for details: https://github.com/mattermost/delivery-platform
workflow_dispatch:
inputs:
pr_number:
description: "PR number to test (for manual triggers)"
type: string
required: false
commit_sha:
description: "Commit SHA to test (for Argo Events)"
type: string
required: false
required: true
jobs:
resolve-pr:
runs-on: ubuntu-24.04
outputs:
PR_NUMBER: "${{ steps.resolve.outputs.PR_NUMBER }}"
COMMIT_SHA: "${{ steps.resolve.outputs.COMMIT_SHA }}"
SERVER_IMAGE_TAG: "${{ steps.e2e-check.outputs.image_tag }}"
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: ci/resolve-pr-and-commit
id: resolve
env:
GH_TOKEN: ${{ github.token }}
INPUT_PR_NUMBER: ${{ inputs.pr_number || github.event.pull_request.number }}
INPUT_COMMIT_SHA: ${{ inputs.commit_sha || github.event.pull_request.head.sha }}
run: |
# Validate inputs
if [ -n "$INPUT_PR_NUMBER" ] && ! [[ "$INPUT_PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number format. Must be numeric."
exit 1
fi
if [ -n "$INPUT_COMMIT_SHA" ] && ! [[ "$INPUT_COMMIT_SHA" =~ ^[a-f0-9]{7,40}$ ]]; then
echo "::error::Invalid commit SHA format. Must be 7-40 hex characters."
exit 1
fi
# Manual trigger: PR number provided, resolve commit SHA from PR head
if [ -n "$INPUT_PR_NUMBER" ]; then
echo "Manual trigger: resolving commit SHA from PR #${INPUT_PR_NUMBER}"
PR_DATA=$(gh api "repos/${{ github.repository }}/pulls/${INPUT_PR_NUMBER}")
COMMIT_SHA=$(echo "$PR_DATA" | jq -r '.head.sha')
if [ -z "$COMMIT_SHA" ] || [ "$COMMIT_SHA" = "null" ]; then
echo "::error::Could not resolve commit SHA for PR #${INPUT_PR_NUMBER}"
exit 1
fi
echo "PR_NUMBER=${INPUT_PR_NUMBER}" >> $GITHUB_OUTPUT
echo "COMMIT_SHA=${COMMIT_SHA}" >> $GITHUB_OUTPUT
exit 0
fi
# Argo Events trigger: commit SHA provided, resolve PR number
if [ -n "$INPUT_COMMIT_SHA" ]; then
echo "Automated trigger: resolving PR number from commit ${INPUT_COMMIT_SHA}"
PR_DATA=$(gh api "repos/${{ github.repository }}/commits/${INPUT_COMMIT_SHA}/pulls" \
--jq '.[0] // empty' 2>/dev/null || echo "")
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number // empty' 2>/dev/null || echo "")
if [ -z "$PR_NUMBER" ]; then
echo "::error::No PR found for commit ${INPUT_COMMIT_SHA}. This workflow is for PRs only."
exit 1
fi
echo "Found PR #${PR_NUMBER} for commit ${INPUT_COMMIT_SHA}"
# Skip if PR is already merged to master or a release branch.
# The e2e-tests-on-merge workflow handles post-merge E2E tests.
PR_MERGED=$(echo "$PR_DATA" | jq -r '.merged_at // empty' 2>/dev/null || echo "")
PR_BASE_REF=$(echo "$PR_DATA" | jq -r '.base.ref // empty' 2>/dev/null || echo "")
if [ -n "$PR_MERGED" ]; then
if [ "$PR_BASE_REF" = "master" ] || [[ "$PR_BASE_REF" =~ ^release-[0-9]+\.[0-9]+$ ]]; then
echo "PR #${PR_NUMBER} is already merged to ${PR_BASE_REF}. Skipping - handled by e2e-tests-on-merge workflow."
echo "PR_NUMBER=" >> $GITHUB_OUTPUT
echo "COMMIT_SHA=" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_OUTPUT
echo "COMMIT_SHA=${INPUT_COMMIT_SHA}" >> $GITHUB_OUTPUT
exit 0
fi
# Neither provided
echo "::error::Either pr_number or commit_sha must be provided"
exit 1
- name: ci/check-e2e-test-only
if: steps.resolve.outputs.PR_NUMBER != ''
id: e2e-check
uses: ./.github/actions/check-e2e-test-only
with:
pr_number: ${{ steps.resolve.outputs.PR_NUMBER }}
check-changes:
needs: resolve-pr
if: needs.resolve-pr.outputs.PR_NUMBER != ''
runs-on: ubuntu-24.04
outputs:
should_run: "${{ steps.check.outputs.should_run }}"
steps:
- name: ci/checkout-repo
if: inputs.commit_sha != '' || github.event.pull_request
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.resolve-pr.outputs.COMMIT_SHA }}
fetch-depth: 0
- name: ci/check-relevant-changes
id: check
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.resolve-pr.outputs.PR_NUMBER }}
COMMIT_SHA: ${{ needs.resolve-pr.outputs.COMMIT_SHA }}
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
run: |
# Manual trigger (pr_number provided): always run E2E tests
if [ -n "$INPUT_PR_NUMBER" ]; then
echo "Manual trigger detected - skipping file change check"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
# Automated trigger (commit_sha provided): check for relevant file changes
echo "Automated trigger detected - checking for relevant file changes"
# Get the base branch of the PR
BASE_SHA=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" --jq '.base.sha')
# Get changed files between base and head
CHANGED_FILES=$(git diff --name-only "${BASE_SHA}...${COMMIT_SHA}")
echo "Changed files:"
echo "$CHANGED_FILES"
# Check for relevant changes
SHOULD_RUN="false"
# Check for server Go files
if echo "$CHANGED_FILES" | grep -qE '^server/.*\.go$'; then
echo "Found server Go file changes"
SHOULD_RUN="true"
fi
# Check for webapp ts/js/tsx/jsx files
if echo "$CHANGED_FILES" | grep -qE '^webapp/.*\.(ts|tsx|js|jsx)$'; then
echo "Found webapp TypeScript/JavaScript file changes"
SHOULD_RUN="true"
fi
# Check for e2e-tests ts/js/tsx/jsx files
if echo "$CHANGED_FILES" | grep -qE '^e2e-tests/.*\.(ts|tsx|js|jsx)$'; then
echo "Found e2e-tests TypeScript/JavaScript file changes"
SHOULD_RUN="true"
fi
echo "should_run=${SHOULD_RUN}" >> $GITHUB_OUTPUT
echo "Should run E2E tests: ${SHOULD_RUN}"
e2e-cypress:
needs:
- resolve-pr
- check-changes
if: needs.resolve-pr.outputs.PR_NUMBER != ''
permissions:
statuses: write
uses: ./.github/workflows/e2e-tests-cypress.yml
e2e-smoketest:
uses: ./.github/workflows/e2e-tests-ci-template.yml
with:
commit_sha: "${{ needs.resolve-pr.outputs.COMMIT_SHA }}"
server: "onprem"
server_image_tag: "${{ needs.resolve-pr.outputs.SERVER_IMAGE_TAG }}"
enable_reporting: true
report_type: "PR"
pr_number: "${{ needs.resolve-pr.outputs.PR_NUMBER }}"
should_run: "${{ needs.check-changes.outputs.should_run }}"
secrets:
MM_LICENSE: "${{ secrets.MM_E2E_TEST_LICENSE_ONPREM_ENT }}"
AUTOMATION_DASHBOARD_URL: "${{ secrets.MM_E2E_AUTOMATION_DASHBOARD_URL }}"
AUTOMATION_DASHBOARD_TOKEN: "${{ secrets.MM_E2E_AUTOMATION_DASHBOARD_TOKEN }}"
PUSH_NOTIFICATION_SERVER: "${{ secrets.MM_E2E_PUSH_NOTIFICATION_SERVER }}"
REPORT_WEBHOOK_URL: "${{ secrets.MM_E2E_REPORT_WEBHOOK_URL }}"
CWS_URL: "${{ secrets.MM_E2E_CWS_URL }}"
CWS_EXTRA_HTTP_HEADERS: "${{ secrets.MM_E2E_CWS_EXTRA_HTTP_HEADERS }}"
e2e-playwright:
needs:
- resolve-pr
- check-changes
if: needs.resolve-pr.outputs.PR_NUMBER != ''
permissions:
statuses: write
uses: ./.github/workflows/e2e-tests-playwright.yml
with:
commit_sha: "${{ needs.resolve-pr.outputs.COMMIT_SHA }}"
server: "onprem"
server_image_tag: "${{ needs.resolve-pr.outputs.SERVER_IMAGE_TAG }}"
enable_reporting: true
report_type: "PR"
pr_number: "${{ needs.resolve-pr.outputs.PR_NUMBER }}"
should_run: "${{ needs.check-changes.outputs.should_run }}"
secrets:
MM_LICENSE: "${{ secrets.MM_E2E_TEST_LICENSE_ONPREM_ENT }}"
AWS_ACCESS_KEY_ID: "${{ secrets.CYPRESS_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.CYPRESS_AWS_SECRET_ACCESS_KEY }}"
REPORT_WEBHOOK_URL: "${{ secrets.MM_E2E_REPORT_WEBHOOK_URL }}"
commit_sha: "${{ inputs.commit_sha }}"
status_check_context: "E2E Tests/smoketests"

Просмотреть файл

@@ -1,649 +0,0 @@
---
name: E2E Tests - Cypress Template
on:
workflow_call:
inputs:
# Test configuration
test_type:
description: "Type of test run (smoke or full)"
type: string
required: true
test_filter:
description: "Test filter arguments"
type: string
required: true
workers:
description: "Number of parallel workers"
type: number
required: false
default: 1
enabled_docker_services:
description: "Space-separated list of docker services to enable"
type: string
required: false
default: "postgres inbucket"
# Common build variables
commit_sha:
type: string
required: true
branch:
type: string
required: true
build_id:
type: string
required: true
server_image_tag:
description: "Server image tag (e.g., master or short SHA)"
type: string
required: true
server:
type: string
required: false
default: onprem
server_edition:
description: "Server edition: enterprise (default), fips, or team"
type: string
required: false
default: enterprise
server_image_repo:
description: "Docker registry: mattermostdevelopment (default) or mattermost"
type: string
required: false
default: mattermostdevelopment
server_image_aliases:
description: "Comma-separated alias tags for description (e.g., 'release-11.4, release-11')"
type: string
required: false
# Reporting options
enable_reporting:
type: boolean
required: false
default: false
report_type:
type: string
required: false
ref_branch:
description: "Source branch name for webhook messages (e.g., 'master' or 'release-11.4')"
type: string
required: false
pr_number:
type: string
required: false
# Commit status configuration
context_name:
description: "GitHub commit status context name"
type: string
required: true
outputs:
passed:
description: "Number of passed tests"
value: ${{ jobs.report.outputs.passed }}
failed:
description: "Number of failed tests"
value: ${{ jobs.report.outputs.failed }}
status_check_url:
description: "URL to test results"
value: ${{ jobs.generate-test-cycle.outputs.status_check_url }}
secrets:
MM_LICENSE:
required: false
AUTOMATION_DASHBOARD_URL:
required: false
AUTOMATION_DASHBOARD_TOKEN:
required: false
PUSH_NOTIFICATION_SERVER:
required: false
REPORT_WEBHOOK_URL:
required: false
CWS_URL:
required: false
CWS_EXTRA_HTTP_HEADERS:
required: false
env:
SERVER_IMAGE: "${{ inputs.server_image_repo }}/${{ inputs.server_edition == 'fips' && 'mattermost-enterprise-fips-edition' || inputs.server_edition == 'team' && 'mattermost-team-edition' || 'mattermost-enterprise-edition' }}:${{ inputs.server_image_tag }}"
jobs:
update-initial-status:
runs-on: ubuntu-24.04
steps:
- name: ci/set-initial-status
uses: mattermost/actions/delivery/update-commit-status@f324ac89b05cc3511cb06e60642ac2fb829f0a63
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.context_name }}
description: "tests running, image_tag:${{ inputs.server_image_tag }}${{ inputs.server_image_aliases && format(' ({0})', inputs.server_image_aliases) || '' }}"
status: pending
generate-test-cycle:
runs-on: ubuntu-24.04
outputs:
status_check_url: "${{ steps.generate-cycle.outputs.status_check_url }}"
workers: "${{ steps.generate-workers.outputs.workers }}"
start_time: "${{ steps.generate-workers.outputs.start_time }}"
steps:
- name: ci/generate-workers
id: generate-workers
run: |
echo "workers=$(jq -nc '[range(${{ inputs.workers }})]')" >> $GITHUB_OUTPUT
echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/cypress/package-lock.json"
- name: ci/generate-test-cycle
id: generate-cycle
working-directory: e2e-tests
env:
AUTOMATION_DASHBOARD_URL: "${{ secrets.AUTOMATION_DASHBOARD_URL }}"
AUTOMATION_DASHBOARD_TOKEN: "${{ secrets.AUTOMATION_DASHBOARD_TOKEN }}"
BRANCH: "${{ inputs.branch }}-${{ inputs.test_type }}"
BUILD_ID: "${{ inputs.build_id }}"
TEST: cypress
TEST_FILTER: "${{ inputs.test_filter }}"
run: |
set -e -o pipefail
make generate-test-cycle | tee generate-test-cycle.out
TEST_CYCLE_ID=$(sed -nE "s/^.*id: '([^']+)'.*$/\1/p" <generate-test-cycle.out)
if [ -n "$TEST_CYCLE_ID" ]; then
echo "status_check_url=https://automation-dashboard.vercel.app/cycles/${TEST_CYCLE_ID}" >> $GITHUB_OUTPUT
else
echo "status_check_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
fi
run-tests:
runs-on: ubuntu-24.04
timeout-minutes: 30
continue-on-error: ${{ inputs.workers > 1 }}
needs:
- generate-test-cycle
if: needs.generate-test-cycle.result == 'success'
strategy:
fail-fast: false
matrix:
worker_index: ${{ fromJSON(needs.generate-test-cycle.outputs.workers) }}
defaults:
run:
working-directory: e2e-tests
env:
AUTOMATION_DASHBOARD_URL: "${{ secrets.AUTOMATION_DASHBOARD_URL }}"
AUTOMATION_DASHBOARD_TOKEN: "${{ secrets.AUTOMATION_DASHBOARD_TOKEN }}"
SERVER: "${{ inputs.server }}"
MM_LICENSE: "${{ secrets.MM_LICENSE }}"
ENABLED_DOCKER_SERVICES: "${{ inputs.enabled_docker_services }}"
TEST: cypress
TEST_FILTER: "${{ inputs.test_filter }}"
BRANCH: "${{ inputs.branch }}-${{ inputs.test_type }}"
BUILD_ID: "${{ inputs.build_id }}"
CI_BASE_URL: "${{ inputs.test_type }}-test-${{ matrix.worker_index }}"
CYPRESS_pushNotificationServer: "${{ secrets.PUSH_NOTIFICATION_SERVER }}"
CWS_URL: "${{ secrets.CWS_URL }}"
CWS_EXTRA_HTTP_HEADERS: "${{ secrets.CWS_EXTRA_HTTP_HEADERS }}"
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/cypress/package-lock.json"
- name: ci/run-tests
run: |
make cloud-init
make
- name: ci/cloud-teardown
if: always()
run: make cloud-teardown
- name: ci/upload-results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: cypress-${{ inputs.test_type }}-${{ inputs.server_edition }}-results-${{ matrix.worker_index }}
path: |
e2e-tests/cypress/logs/
e2e-tests/cypress/results/
retention-days: 5
calculate-results:
runs-on: ubuntu-24.04
needs:
- generate-test-cycle
- run-tests
if: always() && needs.generate-test-cycle.result == 'success'
outputs:
passed: ${{ steps.calculate.outputs.passed }}
failed: ${{ steps.calculate.outputs.failed }}
pending: ${{ steps.calculate.outputs.pending }}
total_specs: ${{ steps.calculate.outputs.total_specs }}
failed_specs: ${{ steps.calculate.outputs.failed_specs }}
failed_specs_count: ${{ steps.calculate.outputs.failed_specs_count }}
failed_tests: ${{ steps.calculate.outputs.failed_tests }}
commit_status_message: ${{ steps.calculate.outputs.commit_status_message }}
total: ${{ steps.calculate.outputs.total }}
pass_rate: ${{ steps.calculate.outputs.pass_rate }}
color: ${{ steps.calculate.outputs.color }}
test_duration: ${{ steps.calculate.outputs.test_duration }}
end_time: ${{ steps.record-end-time.outputs.end_time }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ci/download-results
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
pattern: cypress-${{ inputs.test_type }}-${{ inputs.server_edition }}-results-*
path: e2e-tests/cypress/
merge-multiple: true
- name: ci/calculate
id: calculate
uses: ./.github/actions/calculate-cypress-results
with:
original-results-path: e2e-tests/cypress/results
- name: ci/record-end-time
id: record-end-time
run: echo "end_time=$(date +%s)" >> $GITHUB_OUTPUT
run-failed-tests:
runs-on: ubuntu-24.04
timeout-minutes: 30
needs:
- generate-test-cycle
- run-tests
- calculate-results
if: >-
always() &&
needs.calculate-results.result == 'success' &&
needs.calculate-results.outputs.failed != '0' &&
fromJSON(needs.calculate-results.outputs.failed_specs_count) <= 20
defaults:
run:
working-directory: e2e-tests
env:
AUTOMATION_DASHBOARD_URL: "${{ secrets.AUTOMATION_DASHBOARD_URL }}"
AUTOMATION_DASHBOARD_TOKEN: "${{ secrets.AUTOMATION_DASHBOARD_TOKEN }}"
SERVER: "${{ inputs.server }}"
MM_LICENSE: "${{ secrets.MM_LICENSE }}"
ENABLED_DOCKER_SERVICES: "${{ inputs.enabled_docker_services }}"
TEST: cypress
BRANCH: "${{ inputs.branch }}-${{ inputs.test_type }}-retest"
BUILD_ID: "${{ inputs.build_id }}-retest"
CYPRESS_pushNotificationServer: "${{ secrets.PUSH_NOTIFICATION_SERVER }}"
CWS_URL: "${{ secrets.CWS_URL }}"
CWS_EXTRA_HTTP_HEADERS: "${{ secrets.CWS_EXTRA_HTTP_HEADERS }}"
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/cypress/package-lock.json"
- name: ci/run-failed-specs
env:
SPEC_FILES: ${{ needs.calculate-results.outputs.failed_specs }}
run: |
echo "Retesting failed specs: $SPEC_FILES"
make cloud-init
make start-server run-specs
- name: ci/cloud-teardown
if: always()
run: make cloud-teardown
- name: ci/upload-retest-results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: cypress-${{ inputs.test_type }}-${{ inputs.server_edition }}-retest-results
path: |
e2e-tests/cypress/logs/
e2e-tests/cypress/results/
retention-days: 5
report:
runs-on: ubuntu-24.04
needs:
- generate-test-cycle
- run-tests
- calculate-results
- run-failed-tests
if: always() && needs.calculate-results.result == 'success'
outputs:
passed: "${{ steps.final-results.outputs.passed }}"
failed: "${{ steps.final-results.outputs.failed }}"
commit_status_message: "${{ steps.final-results.outputs.commit_status_message }}"
duration: "${{ steps.duration.outputs.duration }}"
duration_display: "${{ steps.duration.outputs.duration_display }}"
retest_display: "${{ steps.duration.outputs.retest_display }}"
defaults:
run:
working-directory: e2e-tests
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ci/setup-node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/cypress/package-lock.json"
# PATH A: run-failed-tests was skipped (no failures to retest)
- name: ci/download-results-path-a
if: needs.run-failed-tests.result == 'skipped'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
pattern: cypress-${{ inputs.test_type }}-${{ inputs.server_edition }}-results-*
path: e2e-tests/cypress/
merge-multiple: true
- name: ci/use-previous-calculation
if: needs.run-failed-tests.result == 'skipped'
id: use-previous
run: |
echo "passed=${{ needs.calculate-results.outputs.passed }}" >> $GITHUB_OUTPUT
echo "failed=${{ needs.calculate-results.outputs.failed }}" >> $GITHUB_OUTPUT
echo "pending=${{ needs.calculate-results.outputs.pending }}" >> $GITHUB_OUTPUT
echo "total_specs=${{ needs.calculate-results.outputs.total_specs }}" >> $GITHUB_OUTPUT
echo "failed_specs=${{ needs.calculate-results.outputs.failed_specs }}" >> $GITHUB_OUTPUT
echo "failed_specs_count=${{ needs.calculate-results.outputs.failed_specs_count }}" >> $GITHUB_OUTPUT
echo "commit_status_message=${{ needs.calculate-results.outputs.commit_status_message }}" >> $GITHUB_OUTPUT
echo "total=${{ needs.calculate-results.outputs.total }}" >> $GITHUB_OUTPUT
echo "pass_rate=${{ needs.calculate-results.outputs.pass_rate }}" >> $GITHUB_OUTPUT
echo "color=${{ needs.calculate-results.outputs.color }}" >> $GITHUB_OUTPUT
echo "test_duration=${{ needs.calculate-results.outputs.test_duration }}" >> $GITHUB_OUTPUT
{
echo "failed_tests<<EOF"
echo "${{ needs.calculate-results.outputs.failed_tests }}"
echo "EOF"
} >> $GITHUB_OUTPUT
# PATH B: run-failed-tests ran, need to merge and recalculate
- name: ci/download-original-results
if: needs.run-failed-tests.result != 'skipped'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
pattern: cypress-${{ inputs.test_type }}-${{ inputs.server_edition }}-results-*
path: e2e-tests/cypress/
merge-multiple: true
- name: ci/download-retest-results
if: needs.run-failed-tests.result != 'skipped'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: cypress-${{ inputs.test_type }}-${{ inputs.server_edition }}-retest-results
path: e2e-tests/cypress/retest-results/
- name: ci/calculate-results
if: needs.run-failed-tests.result != 'skipped'
id: recalculate
uses: ./.github/actions/calculate-cypress-results
with:
original-results-path: e2e-tests/cypress/results
retest-results-path: e2e-tests/cypress/retest-results/results
# Set final outputs from either path
- name: ci/set-final-results
id: final-results
env:
USE_PREVIOUS_FAILED_TESTS: ${{ steps.use-previous.outputs.failed_tests }}
RECALCULATE_FAILED_TESTS: ${{ steps.recalculate.outputs.failed_tests }}
run: |
if [ "${{ needs.run-failed-tests.result }}" == "skipped" ]; then
echo "passed=${{ steps.use-previous.outputs.passed }}" >> $GITHUB_OUTPUT
echo "failed=${{ steps.use-previous.outputs.failed }}" >> $GITHUB_OUTPUT
echo "pending=${{ steps.use-previous.outputs.pending }}" >> $GITHUB_OUTPUT
echo "total_specs=${{ steps.use-previous.outputs.total_specs }}" >> $GITHUB_OUTPUT
echo "failed_specs=${{ steps.use-previous.outputs.failed_specs }}" >> $GITHUB_OUTPUT
echo "failed_specs_count=${{ steps.use-previous.outputs.failed_specs_count }}" >> $GITHUB_OUTPUT
echo "commit_status_message=${{ steps.use-previous.outputs.commit_status_message }}" >> $GITHUB_OUTPUT
echo "total=${{ steps.use-previous.outputs.total }}" >> $GITHUB_OUTPUT
echo "pass_rate=${{ steps.use-previous.outputs.pass_rate }}" >> $GITHUB_OUTPUT
echo "color=${{ steps.use-previous.outputs.color }}" >> $GITHUB_OUTPUT
echo "test_duration=${{ steps.use-previous.outputs.test_duration }}" >> $GITHUB_OUTPUT
{
echo "failed_tests<<EOF"
echo "$USE_PREVIOUS_FAILED_TESTS"
echo "EOF"
} >> $GITHUB_OUTPUT
else
echo "passed=${{ steps.recalculate.outputs.passed }}" >> $GITHUB_OUTPUT
echo "failed=${{ steps.recalculate.outputs.failed }}" >> $GITHUB_OUTPUT
echo "pending=${{ steps.recalculate.outputs.pending }}" >> $GITHUB_OUTPUT
echo "total_specs=${{ steps.recalculate.outputs.total_specs }}" >> $GITHUB_OUTPUT
echo "failed_specs=${{ steps.recalculate.outputs.failed_specs }}" >> $GITHUB_OUTPUT
echo "failed_specs_count=${{ steps.recalculate.outputs.failed_specs_count }}" >> $GITHUB_OUTPUT
echo "commit_status_message=${{ steps.recalculate.outputs.commit_status_message }}" >> $GITHUB_OUTPUT
echo "total=${{ steps.recalculate.outputs.total }}" >> $GITHUB_OUTPUT
echo "pass_rate=${{ steps.recalculate.outputs.pass_rate }}" >> $GITHUB_OUTPUT
echo "color=${{ steps.recalculate.outputs.color }}" >> $GITHUB_OUTPUT
echo "test_duration=${{ steps.recalculate.outputs.test_duration }}" >> $GITHUB_OUTPUT
{
echo "failed_tests<<EOF"
echo "$RECALCULATE_FAILED_TESTS"
echo "EOF"
} >> $GITHUB_OUTPUT
fi
- name: ci/compute-duration
id: duration
env:
START_TIME: ${{ needs.generate-test-cycle.outputs.start_time }}
FIRST_PASS_END_TIME: ${{ needs.calculate-results.outputs.end_time }}
RETEST_RESULT: ${{ needs.run-failed-tests.result }}
RETEST_SPEC_COUNT: ${{ needs.calculate-results.outputs.failed_specs_count }}
TEST_DURATION: ${{ steps.final-results.outputs.test_duration }}
run: |
NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
MINUTES=$((ELAPSED / 60))
SECONDS=$((ELAPSED % 60))
DURATION="${MINUTES}m ${SECONDS}s"
# Compute first-pass and re-run durations
FIRST_PASS_ELAPSED=$((FIRST_PASS_END_TIME - START_TIME))
FP_MIN=$((FIRST_PASS_ELAPSED / 60))
FP_SEC=$((FIRST_PASS_ELAPSED % 60))
FIRST_PASS="${FP_MIN}m ${FP_SEC}s"
if [ "$RETEST_RESULT" != "skipped" ]; then
RERUN_ELAPSED=$((NOW - FIRST_PASS_END_TIME))
RR_MIN=$((RERUN_ELAPSED / 60))
RR_SEC=$((RERUN_ELAPSED % 60))
RUN_BREAKDOWN=" (first-pass: ${FIRST_PASS}, re-run: ${RR_MIN}m ${RR_SEC}s)"
else
RUN_BREAKDOWN=""
fi
# Duration icons: >20m high alert, >15m warning, otherwise clock
if [ "$MINUTES" -ge 20 ]; then
DURATION_DISPLAY=":rotating_light: ${DURATION}${RUN_BREAKDOWN} | test: ${TEST_DURATION}"
elif [ "$MINUTES" -ge 15 ]; then
DURATION_DISPLAY=":warning: ${DURATION}${RUN_BREAKDOWN} | test: ${TEST_DURATION}"
else
DURATION_DISPLAY=":clock3: ${DURATION}${RUN_BREAKDOWN} | test: ${TEST_DURATION}"
fi
# Retest indicator with spec count
if [ "$RETEST_RESULT" != "skipped" ]; then
RETEST_DISPLAY=":repeat: re-run ${RETEST_SPEC_COUNT} spec(s)"
else
RETEST_DISPLAY=""
fi
echo "duration=${DURATION}" >> $GITHUB_OUTPUT
echo "duration_display=${DURATION_DISPLAY}" >> $GITHUB_OUTPUT
echo "retest_display=${RETEST_DISPLAY}" >> $GITHUB_OUTPUT
- name: ci/upload-combined-results
if: inputs.workers > 1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: cypress-${{ inputs.test_type }}-${{ inputs.server_edition }}-results
path: |
e2e-tests/cypress/logs/
e2e-tests/cypress/results/
- name: ci/publish-report
if: inputs.enable_reporting && env.REPORT_WEBHOOK_URL != ''
env:
REPORT_WEBHOOK_URL: ${{ secrets.REPORT_WEBHOOK_URL }}
COMMIT_STATUS_MESSAGE: ${{ steps.final-results.outputs.commit_status_message }}
COLOR: ${{ steps.final-results.outputs.color }}
REPORT_URL: ${{ needs.generate-test-cycle.outputs.status_check_url }}
TEST_TYPE: ${{ inputs.test_type }}
REPORT_TYPE: ${{ inputs.report_type }}
COMMIT_SHA: ${{ inputs.commit_sha }}
REF_BRANCH: ${{ inputs.ref_branch }}
PR_NUMBER: ${{ inputs.pr_number }}
DURATION_DISPLAY: ${{ steps.duration.outputs.duration_display }}
RETEST_DISPLAY: ${{ steps.duration.outputs.retest_display }}
run: |
# Capitalize test type
TEST_TYPE_CAP=$(echo "$TEST_TYPE" | sed 's/.*/\u&/')
# Build source line based on report type
COMMIT_SHORT="${COMMIT_SHA::7}"
COMMIT_URL="https://github.com/${{ github.repository }}/commit/${COMMIT_SHA}"
if [ "$REPORT_TYPE" = "RELEASE_CUT" ]; then
SOURCE_LINE=":github_round: [${COMMIT_SHORT}](${COMMIT_URL}) on \`${REF_BRANCH}\`"
elif [ "$REPORT_TYPE" = "MASTER" ] || [ "$REPORT_TYPE" = "RELEASE" ]; then
SOURCE_LINE=":git_merge: [${COMMIT_SHORT}](${COMMIT_URL}) on \`${REF_BRANCH}\`"
else
SOURCE_LINE=":open-pull-request: [mattermost-pr-${PR_NUMBER}](https://github.com/${{ github.repository }}/pull/${PR_NUMBER})"
fi
# Build retest part for message
RETEST_PART=""
if [ -n "$RETEST_DISPLAY" ]; then
RETEST_PART=" | ${RETEST_DISPLAY}"
fi
# Build payload with attachments
PAYLOAD=$(cat <<EOF
{
"username": "E2E Test",
"icon_url": "https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png",
"attachments": [{
"color": "${COLOR}",
"text": "**Results - Cypress ${TEST_TYPE_CAP} Tests**\n\n${SOURCE_LINE}\n:docker: \`${{ env.SERVER_IMAGE }}\`\n${COMMIT_STATUS_MESSAGE}${RETEST_PART} | [full report](${REPORT_URL})\n${DURATION_DISPLAY}"
}]
}
EOF
)
# Send to webhook
curl -X POST -H "Content-Type: application/json" -d "$PAYLOAD" "$REPORT_WEBHOOK_URL"
- name: ci/write-job-summary
if: always()
env:
STATUS_CHECK_URL: ${{ needs.generate-test-cycle.outputs.status_check_url }}
TEST_TYPE: ${{ inputs.test_type }}
PASSED: ${{ steps.final-results.outputs.passed }}
FAILED: ${{ steps.final-results.outputs.failed }}
PENDING: ${{ steps.final-results.outputs.pending }}
TOTAL_SPECS: ${{ steps.final-results.outputs.total_specs }}
FAILED_SPECS_COUNT: ${{ steps.final-results.outputs.failed_specs_count }}
FAILED_SPECS: ${{ steps.final-results.outputs.failed_specs }}
COMMIT_STATUS_MESSAGE: ${{ steps.final-results.outputs.commit_status_message }}
FAILED_TESTS: ${{ steps.final-results.outputs.failed_tests }}
DURATION_DISPLAY: ${{ steps.duration.outputs.duration_display }}
RETEST_RESULT: ${{ needs.run-failed-tests.result }}
run: |
{
echo "## E2E Test Results - Cypress ${TEST_TYPE}"
echo ""
if [ "$FAILED" = "0" ]; then
echo "All tests passed: **${PASSED} passed**"
else
echo "<details>"
echo "<summary>${FAILED} failed, ${PASSED} passed</summary>"
echo ""
echo "| Test | File |"
echo "|------|------|"
echo "${FAILED_TESTS}"
echo "</details>"
fi
echo ""
echo "### Calculation Outputs"
echo ""
echo "| Output | Value |"
echo "|--------|-------|"
echo "| passed | ${PASSED} |"
echo "| failed | ${FAILED} |"
echo "| pending | ${PENDING} |"
echo "| total_specs | ${TOTAL_SPECS} |"
echo "| failed_specs_count | ${FAILED_SPECS_COUNT} |"
echo "| commit_status_message | ${COMMIT_STATUS_MESSAGE} |"
echo "| failed_specs | ${FAILED_SPECS:-none} |"
echo "| duration | ${DURATION_DISPLAY} |"
if [ "$RETEST_RESULT" != "skipped" ]; then
echo "| retested | Yes |"
else
echo "| retested | No |"
fi
echo ""
echo "---"
echo "[View Full Report](${STATUS_CHECK_URL})"
} >> $GITHUB_STEP_SUMMARY
- name: ci/assert-results
run: |
[ "${{ steps.final-results.outputs.failed }}" = "0" ]
update-success-status:
runs-on: ubuntu-24.04
if: always() && needs.report.result == 'success' && needs.calculate-results.result == 'success'
needs:
- generate-test-cycle
- calculate-results
- report
steps:
- uses: mattermost/actions/delivery/update-commit-status@f324ac89b05cc3511cb06e60642ac2fb829f0a63
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.context_name }}
description: "${{ needs.report.outputs.commit_status_message }}, ${{ needs.report.outputs.duration }}, image_tag:${{ inputs.server_image_tag }}${{ inputs.server_image_aliases && format(' ({0})', inputs.server_image_aliases) || '' }}"
status: success
target_url: ${{ needs.generate-test-cycle.outputs.status_check_url }}
update-failure-status:
runs-on: ubuntu-24.04
if: always() && (needs.report.result != 'success' || needs.calculate-results.result != 'success')
needs:
- generate-test-cycle
- calculate-results
- report
steps:
- uses: mattermost/actions/delivery/update-commit-status@f324ac89b05cc3511cb06e60642ac2fb829f0a63
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.context_name }}
description: "${{ needs.report.outputs.commit_status_message }}, ${{ needs.report.outputs.duration }}, image_tag:${{ inputs.server_image_tag }}${{ inputs.server_image_aliases && format(' ({0})', inputs.server_image_aliases) || '' }}"
status: failure
target_url: ${{ needs.generate-test-cycle.outputs.status_check_url }}

194
.github/workflows/e2e-tests-cypress.yml поставляемый
Просмотреть файл

@@ -1,194 +0,0 @@
---
name: E2E Tests - Cypress
on:
workflow_call:
inputs:
commit_sha:
type: string
required: true
enable_reporting:
type: boolean
required: false
default: false
server:
type: string
required: false
default: onprem
report_type:
type: string
required: false
pr_number:
type: string
required: false
server_image_tag:
type: string
required: false
description: "Server image tag (e.g., master or short SHA)"
server_edition:
type: string
required: false
description: "Server edition: enterprise (default), fips, or team"
server_image_repo:
type: string
required: false
default: mattermostdevelopment
description: "Docker registry: mattermostdevelopment (default) or mattermost"
server_image_aliases:
type: string
required: false
description: "Comma-separated alias tags for context name (e.g., 'release-11.4, release-11')"
ref_branch:
type: string
required: false
description: "Source branch name for webhook messages (e.g., 'master' or 'release-11.4')"
should_run:
type: string
required: false
default: "true"
description: "Set to 'false' to skip tests and post a success status without running E2E"
secrets:
MM_LICENSE:
required: false
AUTOMATION_DASHBOARD_URL:
required: false
AUTOMATION_DASHBOARD_TOKEN:
required: false
PUSH_NOTIFICATION_SERVER:
required: false
REPORT_WEBHOOK_URL:
required: false
CWS_URL:
required: false
CWS_EXTRA_HTTP_HEADERS:
required: false
jobs:
generate-build-variables:
runs-on: ubuntu-24.04
outputs:
branch: "${{ steps.build-vars.outputs.branch }}"
build_id: "${{ steps.build-vars.outputs.build_id }}"
server_image_tag: "${{ steps.build-vars.outputs.server_image_tag }}"
server_image: "${{ steps.build-vars.outputs.server_image }}"
context_suffix: "${{ steps.build-vars.outputs.context_suffix }}"
steps:
- name: ci/generate-build-variables
id: build-vars
env:
COMMIT_SHA: ${{ inputs.commit_sha }}
PR_NUMBER: ${{ inputs.pr_number }}
INPUT_SERVER_IMAGE_TAG: ${{ inputs.server_image_tag }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
# Use provided server_image_tag or derive from commit SHA
if [ -n "$INPUT_SERVER_IMAGE_TAG" ]; then
SERVER_IMAGE_TAG="$INPUT_SERVER_IMAGE_TAG"
else
SERVER_IMAGE_TAG="${COMMIT_SHA::7}"
fi
# Validate server_image_tag format (alphanumeric, dots, hyphens, underscores)
if ! [[ "$SERVER_IMAGE_TAG" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "::error::Invalid server_image_tag format: ${SERVER_IMAGE_TAG}"
exit 1
fi
echo "server_image_tag=${SERVER_IMAGE_TAG}" >> $GITHUB_OUTPUT
# Generate branch name
REF_BRANCH="${{ inputs.ref_branch }}"
if [ -n "$PR_NUMBER" ]; then
echo "branch=server-pr-${PR_NUMBER}" >> $GITHUB_OUTPUT
elif [ -n "$REF_BRANCH" ]; then
echo "branch=server-${REF_BRANCH}-${SERVER_IMAGE_TAG}" >> $GITHUB_OUTPUT
else
echo "branch=server-commit-${SERVER_IMAGE_TAG}" >> $GITHUB_OUTPUT
fi
# Determine server image name
EDITION="${{ inputs.server_edition }}"
REPO="${{ inputs.server_image_repo }}"
REPO="${REPO:-mattermostdevelopment}"
case "$EDITION" in
fips) IMAGE_NAME="mattermost-enterprise-fips-edition" ;;
team) IMAGE_NAME="mattermost-team-edition" ;;
*) IMAGE_NAME="mattermost-enterprise-edition" ;;
esac
SERVER_IMAGE="${REPO}/${IMAGE_NAME}:${SERVER_IMAGE_TAG}"
echo "server_image=${SERVER_IMAGE}" >> $GITHUB_OUTPUT
# Validate server_image_aliases format if provided
ALIASES="${{ inputs.server_image_aliases }}"
if [ -n "$ALIASES" ] && ! [[ "$ALIASES" =~ ^[a-zA-Z0-9._,\ -]+$ ]]; then
echo "::error::Invalid server_image_aliases format: ${ALIASES}"
exit 1
fi
# Generate build ID
if [ -n "$EDITION" ] && [ "$EDITION" != "enterprise" ]; then
echo "build_id=${RUN_ID}_${RUN_ATTEMPT}-${SERVER_IMAGE_TAG}-cypress-onprem-${EDITION}" >> $GITHUB_OUTPUT
else
echo "build_id=${RUN_ID}_${RUN_ATTEMPT}-${SERVER_IMAGE_TAG}-cypress-onprem-ent" >> $GITHUB_OUTPUT
fi
# Generate context name suffix based on report type
REPORT_TYPE="${{ inputs.report_type }}"
case "$REPORT_TYPE" in
MASTER) echo "context_suffix=/master" >> $GITHUB_OUTPUT ;;
RELEASE) echo "context_suffix=/release" >> $GITHUB_OUTPUT ;;
RELEASE_CUT) echo "context_suffix=/release-cut" >> $GITHUB_OUTPUT ;;
*) echo "context_suffix=" >> $GITHUB_OUTPUT ;;
esac
skip:
needs:
- generate-build-variables
if: inputs.should_run == 'false'
runs-on: ubuntu-24.04
permissions:
statuses: write
steps:
- name: ci/post-skip-status
env:
GH_TOKEN: ${{ github.token }}
COMMIT_SHA: ${{ inputs.commit_sha }}
CONTEXT_NAME: "e2e-test/cypress-full/${{ inputs.server_edition || 'enterprise' }}${{ needs.generate-build-variables.outputs.context_suffix }}"
run: |
gh api repos/${{ github.repository }}/statuses/${COMMIT_SHA} \
-f state=success \
-f context="${CONTEXT_NAME}" \
-f description="No E2E-relevant changes - skipped" \
-f target_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "Posted success for ${CONTEXT_NAME}"
cypress-full:
needs:
- generate-build-variables
if: inputs.should_run != 'false'
uses: ./.github/workflows/e2e-tests-cypress-template.yml
with:
test_type: full
test_filter: '--stage="@prod" --excludeGroup="@te_only,@cloud_only,@high_availability" --sortFirst="@compliance_export,@elasticsearch,@ldap_group,@ldap" --sortLast="@saml,@keycloak,@plugin,@plugins_uninstall,@mfa,@license_removal"'
workers: 40
enabled_docker_services: "postgres inbucket minio openldap elasticsearch keycloak"
commit_sha: ${{ inputs.commit_sha }}
branch: ${{ needs.generate-build-variables.outputs.branch }}
build_id: ${{ needs.generate-build-variables.outputs.build_id }}
server_image_tag: ${{ needs.generate-build-variables.outputs.server_image_tag }}
server_edition: ${{ inputs.server_edition }}
server_image_repo: ${{ inputs.server_image_repo }}
server_image_aliases: ${{ inputs.server_image_aliases }}
server: ${{ inputs.server }}
enable_reporting: ${{ inputs.enable_reporting }}
report_type: ${{ inputs.report_type }}
ref_branch: ${{ inputs.ref_branch }}
pr_number: ${{ inputs.pr_number }}
context_name: "e2e-test/cypress-full/${{ inputs.server_edition || 'enterprise' }}${{ needs.generate-build-variables.outputs.context_suffix }}"
secrets:
MM_LICENSE: ${{ secrets.MM_LICENSE }}
AUTOMATION_DASHBOARD_URL: ${{ secrets.AUTOMATION_DASHBOARD_URL }}
AUTOMATION_DASHBOARD_TOKEN: ${{ secrets.AUTOMATION_DASHBOARD_TOKEN }}
PUSH_NOTIFICATION_SERVER: ${{ secrets.PUSH_NOTIFICATION_SERVER }}
REPORT_WEBHOOK_URL: ${{ secrets.REPORT_WEBHOOK_URL }}
CWS_URL: ${{ secrets.CWS_URL }}
CWS_EXTRA_HTTP_HEADERS: ${{ secrets.CWS_EXTRA_HTTP_HEADERS }}

Просмотреть файл

@@ -1,583 +0,0 @@
---
name: E2E Tests - Playwright Template
on:
workflow_call:
inputs:
# Test configuration
test_type:
description: "Type of test run (smoke or full)"
type: string
required: true
test_filter:
description: "Test filter arguments (e.g., --grep @smoke)"
type: string
required: true
workers:
description: "Number of parallel shards"
type: number
required: false
default: 2
enabled_docker_services:
description: "Space-separated list of docker services to enable"
type: string
required: false
default: "postgres inbucket"
# Common build variables
commit_sha:
type: string
required: true
branch:
type: string
required: true
build_id:
type: string
required: true
server_image_tag:
description: "Server image tag (e.g., master or short SHA)"
type: string
required: true
server:
type: string
required: false
default: onprem
server_edition:
description: "Server edition: enterprise (default), fips, or team"
type: string
required: false
default: enterprise
server_image_repo:
description: "Docker registry: mattermostdevelopment (default) or mattermost"
type: string
required: false
default: mattermostdevelopment
server_image_aliases:
description: "Comma-separated alias tags for description (e.g., 'release-11.4, release-11')"
type: string
required: false
# Reporting options
enable_reporting:
type: boolean
required: false
default: false
report_type:
type: string
required: false
ref_branch:
description: "Source branch name for webhook messages (e.g., 'master' or 'release-11.4')"
type: string
required: false
pr_number:
type: string
required: false
# Commit status configuration
context_name:
description: "GitHub commit status context name"
type: string
required: true
outputs:
passed:
description: "Number of passed tests"
value: ${{ jobs.report.outputs.passed }}
failed:
description: "Number of failed tests"
value: ${{ jobs.report.outputs.failed }}
report_url:
description: "URL to test report on S3"
value: ${{ jobs.report.outputs.report_url }}
secrets:
MM_LICENSE:
required: false
REPORT_WEBHOOK_URL:
required: false
AWS_ACCESS_KEY_ID:
required: true
AWS_SECRET_ACCESS_KEY:
required: true
env:
SERVER_IMAGE: "${{ inputs.server_image_repo }}/${{ inputs.server_edition == 'fips' && 'mattermost-enterprise-fips-edition' || inputs.server_edition == 'team' && 'mattermost-team-edition' || 'mattermost-enterprise-edition' }}:${{ inputs.server_image_tag }}"
jobs:
update-initial-status:
runs-on: ubuntu-24.04
steps:
- name: ci/set-initial-status
uses: mattermost/actions/delivery/update-commit-status@f324ac89b05cc3511cb06e60642ac2fb829f0a63
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.context_name }}
description: "tests running, image_tag:${{ inputs.server_image_tag }}${{ inputs.server_image_aliases && format(' ({0})', inputs.server_image_aliases) || '' }}"
status: pending
generate-test-variables:
runs-on: ubuntu-24.04
outputs:
workers: "${{ steps.generate-workers.outputs.workers }}"
start_time: "${{ steps.generate-workers.outputs.start_time }}"
steps:
- name: ci/generate-workers
id: generate-workers
run: |
echo "workers=$(jq -nc '[range(1; ${{ inputs.workers }} + 1)]')" >> $GITHUB_OUTPUT
echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
run-tests:
runs-on: ubuntu-24.04
timeout-minutes: 30
continue-on-error: true
needs:
- generate-test-variables
if: needs.generate-test-variables.result == 'success'
strategy:
fail-fast: false
matrix:
worker_index: ${{ fromJSON(needs.generate-test-variables.outputs.workers) }}
defaults:
run:
working-directory: e2e-tests
env:
SERVER: "${{ inputs.server }}"
MM_LICENSE: "${{ secrets.MM_LICENSE }}"
ENABLED_DOCKER_SERVICES: "${{ inputs.enabled_docker_services }}"
TEST: playwright
TEST_FILTER: "${{ inputs.test_filter }}"
PW_SHARD: "${{ format('--shard={0}/{1}', matrix.worker_index, inputs.workers) }}"
BRANCH: "${{ inputs.branch }}-${{ inputs.test_type }}"
BUILD_ID: "${{ inputs.build_id }}"
CI_BASE_URL: "${{ inputs.test_type }}-test-${{ matrix.worker_index }}"
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/playwright/package-lock.json"
- name: ci/get-webapp-node-modules
working-directory: webapp
run: make node_modules
- name: ci/run-tests
run: |
make cloud-init
make
- name: ci/cloud-teardown
if: always()
run: make cloud-teardown
- name: ci/upload-results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: playwright-${{ inputs.test_type }}-${{ inputs.server_edition }}-results-${{ matrix.worker_index }}
path: |
e2e-tests/playwright/logs/
e2e-tests/playwright/results/
retention-days: 5
calculate-results:
runs-on: ubuntu-24.04
needs:
- generate-test-variables
- run-tests
if: always() && needs.generate-test-variables.result == 'success'
outputs:
passed: ${{ steps.calculate.outputs.passed }}
failed: ${{ steps.calculate.outputs.failed }}
flaky: ${{ steps.calculate.outputs.flaky }}
skipped: ${{ steps.calculate.outputs.skipped }}
total_specs: ${{ steps.calculate.outputs.total_specs }}
failed_specs: ${{ steps.calculate.outputs.failed_specs }}
failed_specs_count: ${{ steps.calculate.outputs.failed_specs_count }}
failed_tests: ${{ steps.calculate.outputs.failed_tests }}
commit_status_message: ${{ steps.calculate.outputs.commit_status_message }}
total: ${{ steps.calculate.outputs.total }}
pass_rate: ${{ steps.calculate.outputs.pass_rate }}
passing: ${{ steps.calculate.outputs.passing }}
color: ${{ steps.calculate.outputs.color }}
test_duration: ${{ steps.calculate.outputs.test_duration }}
end_time: ${{ steps.record-end-time.outputs.end_time }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ci/setup-node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/playwright/package-lock.json"
- name: ci/download-shard-results
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
pattern: playwright-${{ inputs.test_type }}-${{ inputs.server_edition }}-results-*
path: e2e-tests/playwright/shard-results/
merge-multiple: true
- name: ci/merge-shard-results
working-directory: e2e-tests/playwright
run: |
mkdir -p results/reporter
# Merge blob reports using Playwright merge-reports (per docs)
npm install --no-save @playwright/test
npx playwright merge-reports --config merge.config.mjs ./shard-results/results/blob-report/
- name: ci/calculate
id: calculate
uses: ./.github/actions/calculate-playwright-results
with:
original-results-path: e2e-tests/playwright/results/reporter/results.json
- name: ci/upload-merged-results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: playwright-${{ inputs.test_type }}-${{ inputs.server_edition }}-results
path: e2e-tests/playwright/results/
retention-days: 5
- name: ci/record-end-time
id: record-end-time
run: echo "end_time=$(date +%s)" >> $GITHUB_OUTPUT
run-failed-tests:
runs-on: ubuntu-24.04
timeout-minutes: 30
needs:
- run-tests
- calculate-results
if: >-
always() &&
needs.calculate-results.result == 'success' &&
needs.calculate-results.outputs.failed != '0' &&
fromJSON(needs.calculate-results.outputs.failed_specs_count) <= 20
defaults:
run:
working-directory: e2e-tests
env:
SERVER: "${{ inputs.server }}"
MM_LICENSE: "${{ secrets.MM_LICENSE }}"
ENABLED_DOCKER_SERVICES: "${{ inputs.enabled_docker_services }}"
TEST: playwright
BRANCH: "${{ inputs.branch }}-${{ inputs.test_type }}-retest"
BUILD_ID: "${{ inputs.build_id }}-retest"
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/playwright/package-lock.json"
- name: ci/get-webapp-node-modules
working-directory: webapp
run: make node_modules
- name: ci/run-failed-specs
env:
SPEC_FILES: ${{ needs.calculate-results.outputs.failed_specs }}
run: |
echo "Retesting failed specs: $SPEC_FILES"
make cloud-init
make start-server run-specs
- name: ci/cloud-teardown
if: always()
run: make cloud-teardown
- name: ci/upload-retest-results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: playwright-${{ inputs.test_type }}-${{ inputs.server_edition }}-retest-results
path: |
e2e-tests/playwright/logs/
e2e-tests/playwright/results/
retention-days: 5
report:
runs-on: ubuntu-24.04
needs:
- generate-test-variables
- run-tests
- calculate-results
- run-failed-tests
if: always() && needs.calculate-results.result == 'success'
outputs:
passed: "${{ steps.final-results.outputs.passed }}"
failed: "${{ steps.final-results.outputs.failed }}"
commit_status_message: "${{ steps.final-results.outputs.commit_status_message }}"
report_url: "${{ steps.upload-to-s3.outputs.report_url }}"
duration: "${{ steps.duration.outputs.duration }}"
duration_display: "${{ steps.duration.outputs.duration_display }}"
retest_display: "${{ steps.duration.outputs.retest_display }}"
defaults:
run:
working-directory: e2e-tests
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ci/setup-node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: "e2e-tests/playwright/package-lock.json"
# Download merged results (uploaded by calculate-results)
- name: ci/download-results
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: playwright-${{ inputs.test_type }}-${{ inputs.server_edition }}-results
path: e2e-tests/playwright/results/
# Download retest results (only if retest ran)
- name: ci/download-retest-results
if: needs.run-failed-tests.result != 'skipped'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: playwright-${{ inputs.test_type }}-${{ inputs.server_edition }}-retest-results
path: e2e-tests/playwright/retest-results/
# Calculate results (with optional merge of retest results)
- name: ci/calculate-results
id: final-results
uses: ./.github/actions/calculate-playwright-results
with:
original-results-path: e2e-tests/playwright/results/reporter/results.json
retest-results-path: ${{ needs.run-failed-tests.result != 'skipped' && 'e2e-tests/playwright/retest-results/results/reporter/results.json' || '' }}
- name: ci/aws-configure
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
with:
aws-region: us-east-1
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: ci/upload-to-s3
id: upload-to-s3
env:
AWS_REGION: us-east-1
AWS_S3_BUCKET: mattermost-cypress-report
PR_NUMBER: "${{ inputs.pr_number }}"
RUN_ID: "${{ github.run_id }}"
COMMIT_SHA: "${{ inputs.commit_sha }}"
TEST_TYPE: "${{ inputs.test_type }}"
run: |
LOCAL_RESULTS_PATH="playwright/results/"
# Use PR number if available, otherwise use commit SHA prefix
if [ -n "$PR_NUMBER" ]; then
S3_PATH="server-pr-${PR_NUMBER}/e2e-reports/playwright-${TEST_TYPE}/${RUN_ID}"
else
S3_PATH="server-commit-${COMMIT_SHA::7}/e2e-reports/playwright-${TEST_TYPE}/${RUN_ID}"
fi
if [[ -d "$LOCAL_RESULTS_PATH" ]]; then
aws s3 sync "$LOCAL_RESULTS_PATH" "s3://${AWS_S3_BUCKET}/${S3_PATH}/results/" \
--acl public-read --cache-control "no-cache"
fi
REPORT_URL="https://${AWS_S3_BUCKET}.s3.amazonaws.com/${S3_PATH}/results/reporter/index.html"
echo "report_url=$REPORT_URL" >> "$GITHUB_OUTPUT"
- name: ci/compute-duration
id: duration
env:
START_TIME: ${{ needs.generate-test-variables.outputs.start_time }}
FIRST_PASS_END_TIME: ${{ needs.calculate-results.outputs.end_time }}
RETEST_RESULT: ${{ needs.run-failed-tests.result }}
RETEST_SPEC_COUNT: ${{ needs.calculate-results.outputs.failed_specs_count }}
TEST_DURATION: ${{ steps.final-results.outputs.test_duration }}
run: |
NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
MINUTES=$((ELAPSED / 60))
SECONDS=$((ELAPSED % 60))
DURATION="${MINUTES}m ${SECONDS}s"
# Compute first-pass and re-run durations
FIRST_PASS_ELAPSED=$((FIRST_PASS_END_TIME - START_TIME))
FP_MIN=$((FIRST_PASS_ELAPSED / 60))
FP_SEC=$((FIRST_PASS_ELAPSED % 60))
FIRST_PASS="${FP_MIN}m ${FP_SEC}s"
if [ "$RETEST_RESULT" != "skipped" ]; then
RERUN_ELAPSED=$((NOW - FIRST_PASS_END_TIME))
RR_MIN=$((RERUN_ELAPSED / 60))
RR_SEC=$((RERUN_ELAPSED % 60))
RUN_BREAKDOWN=" (first-pass: ${FIRST_PASS}, re-run: ${RR_MIN}m ${RR_SEC}s)"
else
RUN_BREAKDOWN=""
fi
# Duration icons: >20m high alert, >15m warning, otherwise clock
if [ "$MINUTES" -ge 20 ]; then
DURATION_DISPLAY=":rotating_light: ${DURATION}${RUN_BREAKDOWN} | test: ${TEST_DURATION}"
elif [ "$MINUTES" -ge 15 ]; then
DURATION_DISPLAY=":warning: ${DURATION}${RUN_BREAKDOWN} | test: ${TEST_DURATION}"
else
DURATION_DISPLAY=":clock3: ${DURATION}${RUN_BREAKDOWN} | test: ${TEST_DURATION}"
fi
# Retest indicator with spec count
if [ "$RETEST_RESULT" != "skipped" ]; then
RETEST_DISPLAY=":repeat: re-run ${RETEST_SPEC_COUNT} spec(s)"
else
RETEST_DISPLAY=""
fi
echo "duration=${DURATION}" >> $GITHUB_OUTPUT
echo "duration_display=${DURATION_DISPLAY}" >> $GITHUB_OUTPUT
echo "retest_display=${RETEST_DISPLAY}" >> $GITHUB_OUTPUT
- name: ci/publish-report
if: inputs.enable_reporting && env.REPORT_WEBHOOK_URL != ''
env:
REPORT_WEBHOOK_URL: ${{ secrets.REPORT_WEBHOOK_URL }}
COMMIT_STATUS_MESSAGE: ${{ steps.final-results.outputs.commit_status_message }}
COLOR: ${{ steps.final-results.outputs.color }}
REPORT_URL: ${{ steps.upload-to-s3.outputs.report_url }}
TEST_TYPE: ${{ inputs.test_type }}
REPORT_TYPE: ${{ inputs.report_type }}
COMMIT_SHA: ${{ inputs.commit_sha }}
REF_BRANCH: ${{ inputs.ref_branch }}
PR_NUMBER: ${{ inputs.pr_number }}
DURATION_DISPLAY: ${{ steps.duration.outputs.duration_display }}
RETEST_DISPLAY: ${{ steps.duration.outputs.retest_display }}
run: |
# Capitalize test type
TEST_TYPE_CAP=$(echo "$TEST_TYPE" | sed 's/.*/\u&/')
# Build source line based on report type
COMMIT_SHORT="${COMMIT_SHA::7}"
COMMIT_URL="https://github.com/${{ github.repository }}/commit/${COMMIT_SHA}"
if [ "$REPORT_TYPE" = "RELEASE_CUT" ]; then
SOURCE_LINE=":github_round: [${COMMIT_SHORT}](${COMMIT_URL}) on \`${REF_BRANCH}\`"
elif [ "$REPORT_TYPE" = "MASTER" ] || [ "$REPORT_TYPE" = "RELEASE" ]; then
SOURCE_LINE=":git_merge: [${COMMIT_SHORT}](${COMMIT_URL}) on \`${REF_BRANCH}\`"
else
SOURCE_LINE=":open-pull-request: [mattermost-pr-${PR_NUMBER}](https://github.com/${{ github.repository }}/pull/${PR_NUMBER})"
fi
# Build retest part for message
RETEST_PART=""
if [ -n "$RETEST_DISPLAY" ]; then
RETEST_PART=" | ${RETEST_DISPLAY}"
fi
# Build payload with attachments
PAYLOAD=$(cat <<EOF
{
"username": "E2E Test",
"icon_url": "https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png",
"attachments": [{
"color": "${COLOR}",
"text": "**Results - Playwright ${TEST_TYPE_CAP} Tests**\n\n${SOURCE_LINE}\n:docker: \`${{ env.SERVER_IMAGE }}\`\n${COMMIT_STATUS_MESSAGE}${RETEST_PART} | [full report](${REPORT_URL})\n${DURATION_DISPLAY}"
}]
}
EOF
)
# Send to webhook
curl -X POST -H "Content-Type: application/json" -d "$PAYLOAD" "$REPORT_WEBHOOK_URL"
- name: ci/write-job-summary
if: always()
env:
REPORT_URL: ${{ steps.upload-to-s3.outputs.report_url }}
TEST_TYPE: ${{ inputs.test_type }}
PASSED: ${{ steps.final-results.outputs.passed }}
FAILED: ${{ steps.final-results.outputs.failed }}
FLAKY: ${{ steps.final-results.outputs.flaky }}
SKIPPED: ${{ steps.final-results.outputs.skipped }}
TOTAL_SPECS: ${{ steps.final-results.outputs.total_specs }}
FAILED_SPECS_COUNT: ${{ steps.final-results.outputs.failed_specs_count }}
FAILED_SPECS: ${{ steps.final-results.outputs.failed_specs }}
COMMIT_STATUS_MESSAGE: ${{ steps.final-results.outputs.commit_status_message }}
FAILED_TESTS: ${{ steps.final-results.outputs.failed_tests }}
DURATION_DISPLAY: ${{ steps.duration.outputs.duration_display }}
RETEST_RESULT: ${{ needs.run-failed-tests.result }}
run: |
{
echo "## E2E Test Results - Playwright ${TEST_TYPE}"
echo ""
if [ "$FAILED" = "0" ]; then
echo "All tests passed: **${PASSED} passed**"
else
echo "<details>"
echo "<summary>${FAILED} failed, ${PASSED} passed</summary>"
echo ""
echo "| Test | File |"
echo "|------|------|"
echo "${FAILED_TESTS}"
echo "</details>"
fi
echo ""
echo "### Calculation Outputs"
echo ""
echo "| Output | Value |"
echo "|--------|-------|"
echo "| passed | ${PASSED} |"
echo "| failed | ${FAILED} |"
echo "| flaky | ${FLAKY} |"
echo "| skipped | ${SKIPPED} |"
echo "| total_specs | ${TOTAL_SPECS} |"
echo "| failed_specs_count | ${FAILED_SPECS_COUNT} |"
echo "| commit_status_message | ${COMMIT_STATUS_MESSAGE} |"
echo "| failed_specs | ${FAILED_SPECS:-none} |"
echo "| duration | ${DURATION_DISPLAY} |"
if [ "$RETEST_RESULT" != "skipped" ]; then
echo "| retested | Yes |"
else
echo "| retested | No |"
fi
echo ""
echo "---"
echo "[View Full Report](${REPORT_URL})"
} >> $GITHUB_STEP_SUMMARY
- name: ci/assert-results
run: |
[ "${{ steps.final-results.outputs.failed }}" = "0" ]
update-success-status:
runs-on: ubuntu-24.04
if: always() && needs.report.result == 'success' && needs.calculate-results.result == 'success'
needs:
- calculate-results
- report
steps:
- uses: mattermost/actions/delivery/update-commit-status@f324ac89b05cc3511cb06e60642ac2fb829f0a63
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.context_name }}
description: "${{ needs.report.outputs.commit_status_message }}, ${{ needs.report.outputs.duration }}, image_tag:${{ inputs.server_image_tag }}${{ inputs.server_image_aliases && format(' ({0})', inputs.server_image_aliases) || '' }}"
status: success
target_url: ${{ needs.report.outputs.report_url }}
update-failure-status:
runs-on: ubuntu-24.04
if: always() && (needs.report.result != 'success' || needs.calculate-results.result != 'success')
needs:
- calculate-results
- report
steps:
- uses: mattermost/actions/delivery/update-commit-status@f324ac89b05cc3511cb06e60642ac2fb829f0a63
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.context_name }}
description: "${{ needs.report.outputs.commit_status_message }}, ${{ needs.report.outputs.duration }}, image_tag:${{ inputs.server_image_tag }}${{ inputs.server_image_aliases && format(' ({0})', inputs.server_image_aliases) || '' }}"
status: failure
target_url: ${{ needs.report.outputs.report_url }}

185
.github/workflows/e2e-tests-playwright.yml поставляемый
Просмотреть файл

@@ -1,185 +0,0 @@
---
name: E2E Tests - Playwright
on:
workflow_call:
inputs:
commit_sha:
type: string
required: true
enable_reporting:
type: boolean
required: false
default: false
server:
type: string
required: false
default: onprem
report_type:
type: string
required: false
pr_number:
type: string
required: false
server_image_tag:
type: string
required: false
description: "Server image tag (e.g., master or short SHA)"
server_edition:
type: string
required: false
description: "Server edition: enterprise (default), fips, or team"
server_image_repo:
type: string
required: false
default: mattermostdevelopment
description: "Docker registry: mattermostdevelopment (default) or mattermost"
server_image_aliases:
type: string
required: false
description: "Comma-separated alias tags for context name (e.g., 'release-11.4, release-11')"
ref_branch:
type: string
required: false
description: "Source branch name for webhook messages (e.g., 'master' or 'release-11.4')"
should_run:
type: string
required: false
default: "true"
description: "Set to 'false' to skip tests and post a success status without running E2E"
secrets:
MM_LICENSE:
required: false
REPORT_WEBHOOK_URL:
required: false
AWS_ACCESS_KEY_ID:
required: true
AWS_SECRET_ACCESS_KEY:
required: true
jobs:
generate-build-variables:
runs-on: ubuntu-24.04
outputs:
branch: "${{ steps.build-vars.outputs.branch }}"
build_id: "${{ steps.build-vars.outputs.build_id }}"
server_image_tag: "${{ steps.build-vars.outputs.server_image_tag }}"
server_image: "${{ steps.build-vars.outputs.server_image }}"
context_suffix: "${{ steps.build-vars.outputs.context_suffix }}"
steps:
- name: ci/generate-build-variables
id: build-vars
env:
COMMIT_SHA: ${{ inputs.commit_sha }}
PR_NUMBER: ${{ inputs.pr_number }}
INPUT_SERVER_IMAGE_TAG: ${{ inputs.server_image_tag }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
# Use provided server_image_tag or derive from commit SHA
if [ -n "$INPUT_SERVER_IMAGE_TAG" ]; then
SERVER_IMAGE_TAG="$INPUT_SERVER_IMAGE_TAG"
else
SERVER_IMAGE_TAG="${COMMIT_SHA::7}"
fi
# Validate server_image_tag format (alphanumeric, dots, hyphens, underscores)
if ! [[ "$SERVER_IMAGE_TAG" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "::error::Invalid server_image_tag format: ${SERVER_IMAGE_TAG}"
exit 1
fi
echo "server_image_tag=${SERVER_IMAGE_TAG}" >> $GITHUB_OUTPUT
# Generate branch name
REF_BRANCH="${{ inputs.ref_branch }}"
if [ -n "$PR_NUMBER" ]; then
echo "branch=server-pr-${PR_NUMBER}" >> $GITHUB_OUTPUT
elif [ -n "$REF_BRANCH" ]; then
echo "branch=server-${REF_BRANCH}-${SERVER_IMAGE_TAG}" >> $GITHUB_OUTPUT
else
echo "branch=server-commit-${SERVER_IMAGE_TAG}" >> $GITHUB_OUTPUT
fi
# Determine server image name
EDITION="${{ inputs.server_edition }}"
REPO="${{ inputs.server_image_repo }}"
REPO="${REPO:-mattermostdevelopment}"
case "$EDITION" in
fips) IMAGE_NAME="mattermost-enterprise-fips-edition" ;;
team) IMAGE_NAME="mattermost-team-edition" ;;
*) IMAGE_NAME="mattermost-enterprise-edition" ;;
esac
SERVER_IMAGE="${REPO}/${IMAGE_NAME}:${SERVER_IMAGE_TAG}"
echo "server_image=${SERVER_IMAGE}" >> $GITHUB_OUTPUT
# Validate server_image_aliases format if provided
ALIASES="${{ inputs.server_image_aliases }}"
if [ -n "$ALIASES" ] && ! [[ "$ALIASES" =~ ^[a-zA-Z0-9._,\ -]+$ ]]; then
echo "::error::Invalid server_image_aliases format: ${ALIASES}"
exit 1
fi
# Generate build ID
if [ -n "$EDITION" ] && [ "$EDITION" != "enterprise" ]; then
echo "build_id=${RUN_ID}_${RUN_ATTEMPT}-${SERVER_IMAGE_TAG}-playwright-onprem-${EDITION}" >> $GITHUB_OUTPUT
else
echo "build_id=${RUN_ID}_${RUN_ATTEMPT}-${SERVER_IMAGE_TAG}-playwright-onprem-ent" >> $GITHUB_OUTPUT
fi
# Generate context name suffix based on report type
REPORT_TYPE="${{ inputs.report_type }}"
case "$REPORT_TYPE" in
MASTER) echo "context_suffix=/master" >> $GITHUB_OUTPUT ;;
RELEASE) echo "context_suffix=/release" >> $GITHUB_OUTPUT ;;
RELEASE_CUT) echo "context_suffix=/release-cut" >> $GITHUB_OUTPUT ;;
*) echo "context_suffix=" >> $GITHUB_OUTPUT ;;
esac
skip:
needs:
- generate-build-variables
if: inputs.should_run == 'false'
runs-on: ubuntu-24.04
permissions:
statuses: write
steps:
- name: ci/post-skip-status
env:
GH_TOKEN: ${{ github.token }}
COMMIT_SHA: ${{ inputs.commit_sha }}
CONTEXT_NAME: "e2e-test/playwright-full/${{ inputs.server_edition || 'enterprise' }}${{ needs.generate-build-variables.outputs.context_suffix }}"
run: |
gh api repos/${{ github.repository }}/statuses/${COMMIT_SHA} \
-f state=success \
-f context="${CONTEXT_NAME}" \
-f description="No E2E-relevant changes - skipped" \
-f target_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "Posted success for ${CONTEXT_NAME}"
playwright-full:
needs:
- generate-build-variables
if: inputs.should_run != 'false'
uses: ./.github/workflows/e2e-tests-playwright-template.yml
with:
test_type: full
test_filter: '--grep-invert "@visual"'
workers: 4
enabled_docker_services: "postgres inbucket minio openldap elasticsearch keycloak"
commit_sha: ${{ inputs.commit_sha }}
branch: ${{ needs.generate-build-variables.outputs.branch }}
build_id: ${{ needs.generate-build-variables.outputs.build_id }}
server_image_tag: ${{ needs.generate-build-variables.outputs.server_image_tag }}
server_edition: ${{ inputs.server_edition }}
server_image_repo: ${{ inputs.server_image_repo }}
server_image_aliases: ${{ inputs.server_image_aliases }}
server: ${{ inputs.server }}
enable_reporting: ${{ inputs.enable_reporting }}
report_type: ${{ inputs.report_type }}
ref_branch: ${{ inputs.ref_branch }}
pr_number: ${{ inputs.pr_number }}
context_name: "e2e-test/playwright-full/${{ inputs.server_edition || 'enterprise' }}${{ needs.generate-build-variables.outputs.context_suffix }}"
secrets:
MM_LICENSE: ${{ secrets.MM_LICENSE }}
REPORT_WEBHOOK_URL: ${{ secrets.REPORT_WEBHOOK_URL }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

150
.github/workflows/e2e-tests-verified-label.yml поставляемый
Просмотреть файл

@@ -1,150 +0,0 @@
---
name: "E2E Tests/verified"
on:
pull_request:
types: [labeled]
env:
REPORT_WEBHOOK_URL: ${{ secrets.MM_E2E_REPORT_WEBHOOK_URL }}
jobs:
approve-e2e:
if: github.event.label.name == 'E2E Tests/verified'
runs-on: ubuntu-24.04
steps:
- name: ci/check-user-permission
id: check-permission
env:
GH_TOKEN: ${{ github.token }}
LABEL_AUTHOR: ${{ github.event.sender.login }}
run: |
# Check if user has write permission to the repository
PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${LABEL_AUTHOR}/permission --jq '.permission' 2>/dev/null || echo "none")
if [[ "$PERMISSION" != "admin" && "$PERMISSION" != "write" ]]; then
echo "User ${LABEL_AUTHOR} doesn't have write permission to the repository (permission: ${PERMISSION})"
exit 1
fi
echo "User ${LABEL_AUTHOR} has ${PERMISSION} permission to the repository"
- name: ci/override-failed-statuses
id: override
env:
GH_TOKEN: ${{ github.token }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Only full tests can be overridden (smoke tests must pass)
FULL_TEST_CONTEXTS=("e2e-test/playwright-full/enterprise" "e2e-test/cypress-full/enterprise")
OVERRIDDEN=""
WEBHOOK_DATA="[]"
for CONTEXT_NAME in "${FULL_TEST_CONTEXTS[@]}"; do
echo "Checking: $CONTEXT_NAME"
# Get current status
STATUS_JSON=$(gh api repos/${{ github.repository }}/commits/${COMMIT_SHA}/statuses \
--jq "[.[] | select(.context == \"$CONTEXT_NAME\")] | first // empty")
if [ -z "$STATUS_JSON" ]; then
echo " No status found, skipping"
continue
fi
CURRENT_DESC=$(echo "$STATUS_JSON" | jq -r '.description // ""')
CURRENT_URL=$(echo "$STATUS_JSON" | jq -r '.target_url // ""')
CURRENT_STATE=$(echo "$STATUS_JSON" | jq -r '.state // ""')
echo " Current: $CURRENT_DESC ($CURRENT_STATE)"
# Only override if status is failure
if [ "$CURRENT_STATE" != "failure" ]; then
echo " Not failed, skipping"
continue
fi
# Prefix existing description
if [ -n "$CURRENT_DESC" ]; then
NEW_MSG="(verified) ${CURRENT_DESC}"
else
NEW_MSG="(verified)"
fi
echo " New: $NEW_MSG"
# Update status via GitHub API
gh api repos/${{ github.repository }}/statuses/${COMMIT_SHA} \
-f state=success \
-f context="$CONTEXT_NAME" \
-f description="$NEW_MSG" \
-f target_url="$CURRENT_URL"
echo " Updated to success"
OVERRIDDEN="${OVERRIDDEN}- ${CONTEXT_NAME}\n"
# Collect data for webhook
TEST_TYPE="unknown"
if [[ "$CONTEXT_NAME" == *"playwright"* ]]; then
TEST_TYPE="playwright"
elif [[ "$CONTEXT_NAME" == *"cypress"* ]]; then
TEST_TYPE="cypress"
fi
WEBHOOK_DATA=$(echo "$WEBHOOK_DATA" | jq \
--arg context "$CONTEXT_NAME" \
--arg test_type "$TEST_TYPE" \
--arg description "$CURRENT_DESC" \
--arg report_url "$CURRENT_URL" \
'. + [{context: $context, test_type: $test_type, description: $description, report_url: $report_url}]')
done
echo "overridden<<EOF" >> $GITHUB_OUTPUT
echo -e "$OVERRIDDEN" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "webhook_data<<EOF" >> $GITHUB_OUTPUT
echo "$WEBHOOK_DATA" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: ci/build-webhook-message
if: env.REPORT_WEBHOOK_URL != '' && steps.override.outputs.overridden != ''
id: webhook-message
env:
WEBHOOK_DATA: ${{ steps.override.outputs.webhook_data }}
run: |
MESSAGE_TEXT=""
while IFS= read -r item; do
[ -z "$item" ] && continue
CONTEXT=$(echo "$item" | jq -r '.context')
DESCRIPTION=$(echo "$item" | jq -r '.description')
REPORT_URL=$(echo "$item" | jq -r '.report_url')
MESSAGE_TEXT="${MESSAGE_TEXT}- **${CONTEXT}**: ${DESCRIPTION}, [view report](${REPORT_URL})\n"
done < <(echo "$WEBHOOK_DATA" | jq -c '.[]')
{
echo "message_text<<EOF"
echo -e "$MESSAGE_TEXT"
echo "EOF"
} >> $GITHUB_OUTPUT
- name: ci/send-webhook-notification
if: env.REPORT_WEBHOOK_URL != '' && steps.override.outputs.overridden != ''
env:
REPORT_WEBHOOK_URL: ${{ env.REPORT_WEBHOOK_URL }}
MESSAGE_TEXT: ${{ steps.webhook-message.outputs.message_text }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_URL: ${{ github.event.pull_request.html_url }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
SENDER: ${{ github.event.sender.login }}
run: |
PAYLOAD=$(cat <<EOF
{
"username": "E2E Test",
"icon_url": "https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png",
"text": "**:white_check_mark: E2E Tests Verified**\n\nBy: \`@${SENDER}\` via \`E2E Tests/verified\` trigger-label\n:open-pull-request: [mattermost-pr-${PR_NUMBER}](${PR_URL}), commit: \`${COMMIT_SHA:0:7}\`\n\n${MESSAGE_TEXT}"
}
EOF
)
curl -X POST -H "Content-Type: application/json" -d "$PAYLOAD" "$REPORT_WEBHOOK_URL"

10
.github/workflows/esrupgrade-common.yml поставляемый
Просмотреть файл

@@ -32,7 +32,6 @@ jobs:
run: |
cd server/build
docker compose --no-ansi run --rm start_dependencies
cat ../tests/custom-schema-cpa.ldif | docker compose --ansi never exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest || true';
cat ../tests/test-data.ldif | docker compose --no-ansi exec -T openldap bash -c 'ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest';
docker compose --no-ansi exec -T minio sh -c 'mkdir -p /data/mattermost-test';
docker compose --no-ansi ps
@@ -90,7 +89,7 @@ jobs:
# We skip the very last line, which simply contains the date of the dump
head -n -1 ${DUMP_SERVER_NAME} | gzip > ${DUMP_SERVER_NAME}.gz
- name: Upload dump
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: upgraded-dump-server
path: ${{ env.DUMP_SERVER_NAME }}.gz
@@ -104,7 +103,6 @@ jobs:
run: |
cd server/build
docker compose --no-ansi run --rm start_dependencies
cat ../tests/custom-schema-cpa.ldif | docker compose --ansi never exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest || true';
cat ../tests/test-data.ldif | docker compose --no-ansi exec -T openldap bash -c 'ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest';
docker compose --no-ansi exec -T minio sh -c 'mkdir -p /data/mattermost-test';
docker compose --no-ansi ps
@@ -136,7 +134,7 @@ jobs:
# We skip the very last line, which simply contains the date of the dump
head -n -1 ${DUMP_SCRIPT_NAME} | gzip > ${DUMP_SCRIPT_NAME}.gz
- name: Upload dump
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: upgraded-dump-script
path: ${{ env.DUMP_SCRIPT_NAME }}.gz
@@ -147,7 +145,7 @@ jobs:
- esr-upgrade-script
steps:
- name: Retrieve dumps
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@v4
- name: Diff dumps
run: |
gzip -d upgraded-dump-server/${DUMP_SERVER_NAME}.gz
@@ -155,7 +153,7 @@ jobs:
diff upgraded-dump-server/$DUMP_SERVER_NAME upgraded-dump-script/$DUMP_SCRIPT_NAME > $DIFF_NAME
- name: Upload diff
if: failure() # Upload the diff only if the previous step failed; i.e., if the diff is non-empty
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: dumps-diff
path: ${{ env.DIFF_NAME }}

2
.github/workflows/i18n-ci-template.yml поставляемый
Просмотреть файл

@@ -15,7 +15,7 @@ jobs:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@480f49412651059a414a6a5c96887abb1877de8a # v45.0.7
uses: tj-actions/changed-files@dcc7a0cba800f454d79fff4b993e8c3555bcc0a8 # v45.0.7
with:
files: |
server/i18n/*.json

2
.github/workflows/migration.yml поставляемый
Просмотреть файл

@@ -66,7 +66,7 @@ jobs:
$TEST_IMAGE \
make test-migration
- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: Migration logs
path: server/migration.log

11
.github/workflows/mmctl-test-template.yml поставляемый
Просмотреть файл

@@ -31,9 +31,9 @@ jobs:
- name: Store required variables for publishing results
run: |
echo "${{ inputs.name }}" > server/test-name
echo "${{ github.event.pull_request.number }}" > server/pr-number
echo "${{ github.event.pull_request.number }}" > server/pr-number
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: server/go.sum
@@ -49,14 +49,12 @@ jobs:
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 || true';
cat ../tests/custom-schema-cpa.ldif | docker compose --ansi never exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest || 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';
docker compose --ansi never exec -T minio sh -c 'mkdir -p /data/mattermost-test';
docker compose --ansi never ps
- name: Run mmctl Tests
env:
BUILD_IMAGE: mattermost/mattermost-build-server:${{ steps.go.outputs.GO_VERSION }}
BUILD_IMAGE: mattermostdevelopment/mattermost-build-server:${{ steps.go.outputs.GO_VERSION }}
run: |
if [[ ${{ github.ref_name }} == 'master' ]]; then
export TESTFLAGS="-timeout 90m -race"
@@ -66,7 +64,6 @@ jobs:
docker run --net ghactions_mm-test \
--ulimit nofile=8096:8096 \
--env-file=server/build/dotenv/test.env \
--env TEST_DATABASE_POSTGRESQL_DSN="${{ inputs.datasource }}" \
--env MM_SQLSETTINGS_DATASOURCE="${{ inputs.datasource }}" \
--env MMCTL_TESTFLAGS="$TESTFLAGS" \
-v $(go env GOCACHE):/go/cache \
@@ -81,7 +78,7 @@ jobs:
docker compose --ansi never stop
- name: Archive logs
if: ${{ always() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: ${{ inputs.logsartifact }}
path: |

6
.github/workflows/scorecards-analysis.yml поставляемый
Просмотреть файл

@@ -26,7 +26,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
with:
results_file: results.sarif
results_format: sarif
@@ -48,7 +48,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: SARIF file
path: results.sarif
@@ -56,6 +56,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v2.27.0
uses: github/codeql-action/upload-sarif@0a35e8f6866a39b001e5f7ad1d0daf9836786896 # v2.27.0
with:
sarif_file: results.sarif

2
.github/workflows/sentry.yaml поставляемый
Просмотреть файл

@@ -20,5 +20,5 @@ jobs:
- name: cd/Checkout mattermost project
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: cd/Create Sentry release
uses: getsentry/action-release@00ed2a6cc2171514e031a0f5b4b3cdc586dc171a # v3.1.1
uses: getsentry/action-release@12bba0bd9c0f65f9f80d4965db646a1aed373d36 # v1.10.3

18
.github/workflows/server-ci-artifacts.yml поставляемый
Просмотреть файл

@@ -17,7 +17,7 @@ jobs:
if: github.repository_owner == 'mattermost' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-22.04
steps:
- uses: mattermost/actions/delivery/update-commit-status@d5174b860704729f4c14ef8489ae075742bfa08a
- uses: mattermost/actions/delivery/update-commit-status@fec7b836001c9380d4bfaf28d443945c103a098c
env:
GITHUB_TOKEN: ${{ github.token }}
with:
@@ -33,14 +33,14 @@ jobs:
- update-initial-status
steps:
- name: cd/configure-aws-credentials
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
uses: aws-actions/configure-aws-credentials@4fc4975a852c8cd99761e2de1f4ba73402e44dd9 # v4.0.3
with:
aws-region: us-east-1
aws-access-key-id: ${{ secrets.PR_BUILDS_BUCKET_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.PR_BUILDS_BUCKET_AWS_SECRET_ACCESS_KEY }}
- name: cd/download-artifacts-from-PR-workflow
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
@@ -77,18 +77,18 @@ jobs:
TAG: ${{ steps.set_tag.outputs.TAG }}
steps:
- name: cd/docker-login
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: mattermostdev
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
- name: cd/setup-cosign
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3.8.0
with:
cosign-release: v${{ env.COSIGN_VERSION }}
- name: cd/download-artifacts-from-PR-workflow
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
@@ -96,7 +96,7 @@ jobs:
path: server/build/
- name: cd/setup-docker-buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
- name: cd/set-docker-tag
id: set_tag
@@ -155,7 +155,7 @@ jobs:
needs:
- build-docker
steps:
- uses: mattermost/actions/delivery/update-commit-status@d5174b860704729f4c14ef8489ae075742bfa08a
- uses: mattermost/actions/delivery/update-commit-status@fec7b836001c9380d4bfaf28d443945c103a098c
env:
GITHUB_TOKEN: ${{ github.token }}
with:
@@ -171,7 +171,7 @@ jobs:
needs:
- build-docker
steps:
- uses: mattermost/actions/delivery/update-commit-status@d5174b860704729f4c14ef8489ae075742bfa08a
- uses: mattermost/actions/delivery/update-commit-status@fec7b836001c9380d4bfaf28d443945c103a098c
env:
GITHUB_TOKEN: ${{ github.token }}
with:

66
.github/workflows/server-ci-report.yml поставляемый
Просмотреть файл

@@ -15,44 +15,21 @@ jobs:
REPORT_MATRIX: ${{ steps.report.outputs.REPORT_MATRIX }}
steps:
- name: report/download-artifacts-from-PR-workflow
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
pattern: "*-test-logs"
path: reports
- name: report/validate-and-prepare-data
id: validate
run: |
# Create validated data file
> /tmp/validated-tests.json
find "reports" -type f -name "test-name" | while read -r test_file; do
folder=$(basename "$(dirname "$test_file")")
test_name_raw=$(cat "$test_file" | tr -d '\n\r')
# Validate test name: allow alphanumeric, spaces, hyphens, underscores, parentheses, and dots
if [[ "$test_name_raw" =~ ^[a-zA-Z0-9\ \(\)_.-]+$ ]] && [[ ${#test_name_raw} -le 100 ]]; then
# Use jq to safely escape the test name as JSON
test_name_escaped=$(echo -n "$test_name_raw" | jq -R .)
echo "{\"artifact\": \"$folder\", \"name\": $test_name_escaped}" >> /tmp/validated-tests.json
else
echo "Warning: Skipping invalid test name in $test_file: '$test_name_raw'" >&2
fi
done
# Verify we have at least some valid tests
if [[ ! -s /tmp/validated-tests.json ]]; then
echo "Error: No valid test names found" >&2
exit 1
fi
- name: report/generate-report-matrix
id: report
run: |
# Convert validated JSON objects to matrix format
jq -s '{ "test": . }' /tmp/validated-tests.json | tee /tmp/report-matrix
find "reports" -type f -name "test-name" | while read -r test_file; do
folder=$(basename "$(dirname "$test_file")")
test_name=$(cat "$test_file")
echo "{\"artifact\": \"$folder\", \"name\": \"$test_name\"}"
done | jq -s '{ "test": . }' | tee /tmp/report-matrix
echo REPORT_MATRIX=$(cat /tmp/report-matrix | jq --compact-output --monochrome-output) >> ${GITHUB_OUTPUT}
publish-report:
@@ -68,7 +45,7 @@ jobs:
matrix: ${{ fromJson(needs.generate-report-matrix.outputs.REPORT_MATRIX) }}
steps:
- name: report/download-artifacts-from-PR-workflow
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
@@ -77,24 +54,10 @@ jobs:
- name: report/fetch-pr-number
if: github.event.workflow_run.name == 'Server CI PR'
id: incoming-pr
env:
ARTIFACT: "${{ matrix.test.artifact }}"
run: |
if [[ -f "$ARTIFACT/pr-number" ]]; then
pr_number=$(cat "$ARTIFACT/pr-number" | tr -d '\n\r' | grep -E '^[0-9]+$')
if [[ -n "$pr_number" ]] && [[ ${#pr_number} -le 10 ]]; then
echo "NUMBER=$pr_number" >> ${GITHUB_OUTPUT}
else
echo "Invalid PR number format" >&2
exit 1
fi
else
echo "PR number file not found" >&2
exit 1
fi
run: echo "NUMBER=$(cat ${{ matrix.test.artifact }}/pr-number)" >> ${GITHUB_OUTPUT}
- name: Publish test report
id: report
uses: mikepenz/action-junit-report@cf701569b05ccdd861a76b8607a66d76f6fd4857 # v5.5.1
uses: mikepenz/action-junit-report@ee6b445351cd81e2f73a16a0e52d598aeac2197f # v5.3.0
with:
report_paths: ${{ matrix.test.artifact }}/report.xml
check_name: ${{ matrix.test.name }} (Results)
@@ -106,6 +69,17 @@ jobs:
include_passed: true
check_annotations: true
- name: Report retried tests via webhook (master)
if: ${{ steps.report.outputs.flaky_summary != '<table><tr><th>Test</th><th>Retries</th></tr></table>' && github.event.workflow_run.name == 'Server CI Master' && github.event.workflow_run.head_branch == 'master' }}
uses: mattermost/action-mattermost-notify@b7d118e440bf2749cd18a4a8c88e7092e696257a # v2.0.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MM_COMMUNITY_DEVELOPERS_INCOMING_WEBHOOK_FROM_GH_ACTIONS }}
TEXT: |-
#### ⚠️ One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:${{ matrix.test.name }}](${{ github.event.workflow_run.html_url }})
* Ideally, this would have been caught in a pull request, but now a volunteer is required. If you're willing to help, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.
* Finally, reply to this message with a link to the created JIRA ticket.
- name: Report retried tests (pull request)
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
if: ${{ steps.report.outputs.flaky_summary != '<table><tr><th>Test</th><th>Retries</th></tr></table>' && github.event.workflow_run.name == 'Server CI PR' }}

64
.github/workflows/server-ci-template.yml поставляемый
Просмотреть файл

@@ -19,7 +19,7 @@ jobs:
id: go
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: |
@@ -44,7 +44,7 @@ jobs:
id: go
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: |
@@ -69,7 +69,7 @@ jobs:
id: go
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: |
@@ -92,7 +92,7 @@ jobs:
id: go
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: |
@@ -117,7 +117,7 @@ jobs:
id: go
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: |
@@ -130,7 +130,7 @@ jobs:
- name: Run plugin-checker
run: make plugin-checker
- name: Run mattermost-vet
run: make vet BUILD_NUMBER='${GITHUB_HEAD_REF}'
run: make vet BUILD_NUMBER='${GITHUB_HEAD_REF}' MM_NO_ENTERPRISE_LINT=true
check-mattermost-vet-api:
name: Vet API
runs-on: ubuntu-22.04
@@ -144,7 +144,7 @@ jobs:
id: go
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: |
@@ -195,7 +195,7 @@ jobs:
id: go
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: |
@@ -220,7 +220,7 @@ jobs:
id: go
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: |
@@ -264,44 +264,6 @@ jobs:
datasource: mmuser:mostest@tcp(mysql:3306)/mattermost_test?charset=utf8mb4&multiStatements=true&maxAllowedPacket=4194304
drivername: mysql
logsartifact: mysql-server-test-logs
test-elasticsearch-v8:
name: Elasticsearch v8 Compatibility
needs: check-mattermost-vet
uses: ./.github/workflows/server-test-template.yml
secrets: inherit
with:
name: Elasticsearch v8 Compatibility
datasource: postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10
drivername: postgres
logsartifact: elasticsearch-v8-server-test-logs
elasticsearch-version: "8.9.0"
test-target: "test-server-elasticsearch"
test-elasticsearch-v7:
name: Elasticsearch v7 Compatibility
needs: check-mattermost-vet
uses: ./.github/workflows/server-test-template.yml
secrets: inherit
with:
name: Elasticsearch v7 Compatibility
datasource: postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10
drivername: postgres
logsartifact: elasticsearch-v7-server-test-logs
elasticsearch-version: "7.17.29"
test-target: "test-server-elasticsearch"
test-coverage:
# Skip coverage generation for cherry-pick PRs into release branches.
if: ${{ github.event_name != 'pull_request' || !startsWith(github.event.pull_request.base.ref, 'release-') }}
name: Generate Test Coverage
needs: check-mattermost-vet
uses: ./.github/workflows/server-test-template.yml
secrets: inherit
with:
name: Generate Test Coverage
datasource: postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10
drivername: postgres
logsartifact: coverage-server-test-logs
fullyparallel: true
enablecoverage: true
test-mmctl:
name: Run mmctl tests
needs: check-mattermost-vet
@@ -326,14 +288,14 @@ jobs:
id: go
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: |
server/go.sum
server/public/go.sum
- name: ci/setup-node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
id: setup_node
with:
node-version-file: ".nvmrc"
@@ -347,7 +309,7 @@ jobs:
make build-cmd BUILD_NUMBER='${GITHUB_HEAD_REF}-${GITHUB_RUN_ID}'
make package BUILD_NUMBER='${GITHUB_HEAD_REF}-${GITHUB_RUN_ID}'
- name: Persist dist artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: server-dist-artifact
path: server/dist/
@@ -355,7 +317,7 @@ jobs:
compression-level: 0
retention-days: 2
- name: Persist build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: server-build-artifact
path: server/build/

150
.github/workflows/server-test-template.yml поставляемый
Просмотреть файл

@@ -14,67 +14,22 @@ on:
logsartifact:
required: true
type: string
fullyparallel:
required: false
type: boolean
default: false
enablecoverage:
required: false
type: boolean
default: false
elasticsearch-version:
required: false
type: string
default: "9.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
permissions:
id-token: write
contents: read
jobs:
test:
name: ${{ inputs.name }}
runs-on: ubuntu-latest-8-cores
continue-on-error: ${{ inputs.fullyparallel }} # Used to avoid blocking PRs in case of flakiness
runs-on: ubuntu-22.04
env:
COMPOSE_PROJECT_NAME: ghactions
steps:
- name: Checkout mattermost project
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- 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.
key: server-test-timing-master
restore-keys: |
server-test-timing-
- name: Calculate Golang Version
id: go
working-directory: ./server
run: echo GO_VERSION=$(cat .go-version) >> "${GITHUB_OUTPUT}"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ steps.go.outputs.GO_VERSION }}
cache-dependency-path: server/go.sum
@@ -83,93 +38,19 @@ jobs:
echo "${{ inputs.name }}" > server/test-name
echo "${{ github.event.pull_request.number }}" > server/pr-number
- name: Run docker compose
env:
ELASTICSEARCH_VERSION: ${{ inputs.elasticsearch-version }}
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 || true';
cat ../tests/custom-schema-cpa.ldif | docker compose --ansi never exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest || 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';
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: ${{ steps.go.outputs.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 }}
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
# ── Run shard solver ──
node scripts/shard-split.js
echo "has_packages=true" >> "$GITHUB_OUTPUT"
- name: Run Tests
env:
BUILD_IMAGE: mattermost/mattermost-build-server:${{ steps.go.outputs.GO_VERSION }}
BUILD_IMAGE: mattermostdevelopment/mattermost-build-server:${{ steps.go.outputs.GO_VERSION }}
run: |
if [[ ${{ github.ref_name }} == 'master' && ${{ inputs.fullyparallel }} != true && "${{ inputs.test-target }}" == "test-server" ]]; then
if [[ ${{ github.ref_name }} == 'master' ]]; then
export RACE_MODE="-race"
fi
TEST_TARGET="${{ inputs.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 [[ "${{ inputs.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 \
@@ -177,40 +58,23 @@ jobs:
--env MM_SQLSETTINGS_DATASOURCE="${{ inputs.datasource }}" \
--env TEST_DATABASE_MYSQL_DSN="${{ inputs.datasource }}" \
--env TEST_DATABASE_POSTGRESQL_DSN="${{ inputs.datasource }}" \
--env ENABLE_FULLY_PARALLEL_TESTS="${{ inputs.fullyparallel }}" \
--env ENABLE_COVERAGE="${{ inputs.enablecoverage }}" \
-v $(go env GOCACHE):/go/cache \
-e GOCACHE=/go/cache \
--env RACE_MODE \
--env TEST_TARGET \
--env BUILD_NUMBER \
-v $PWD:/mattermost \
-w /mattermost/server \
$BUILD_IMAGE \
$DOCKER_CMD
- name: Upload coverage to Codecov
if: ${{ inputs.enablecoverage }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
disable_search: true
files: server/cover.out
make test-server$RACE_MODE BUILD_NUMBER=$GITHUB_HEAD_REF-$GITHUB_RUN_ID
- name: Stop docker compose
if: ${{ always() }}
run: |
cd server/build
docker compose --ansi never logs --no-color > ../../docker-compose.log 2>&1
docker compose --ansi never stop
- name: Archive logs
if: ${{ always() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: ${{ inputs.logsartifact }}
path: |
server/gotestsum.json
server/report.xml
server/cover.out
server/test-name
server/pr-number
docker-compose.log
server/pr-number

11
.github/workflows/snyk-sbom.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
name: Snyk - Software Bill of Materials (SBOM)
on:
release:
types: [published]
jobs:
sbom:
permissions:
contents: write
uses: mattermost/actions-workflows/.github/workflows/snyk-sbom.yml@9f3b82abb56fb8327b6b6e2d3fc16b92e45120ba
secrets: inherit

2
.github/workflows/tag-public-module.yaml поставляемый
Просмотреть файл

@@ -51,7 +51,7 @@ jobs:
level: ${{ inputs.semver_bump }}
- name: release/create-annotated-tag
uses: mattermost/actions/delivery/create-tag@d5174b860704729f4c14ef8489ae075742bfa08a
uses: mattermost/actions/delivery/create-tag@93502485eec8db2fd67b54c71a02462e06d0fa46
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

2
.github/workflows/webapp-ci-master.yml поставляемый
Просмотреть файл

@@ -8,5 +8,3 @@ on:
jobs:
master-ci:
uses: ./.github/workflows/webapp-ci-template.yml
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

2
.github/workflows/webapp-ci-pr.yml поставляемый
Просмотреть файл

@@ -18,5 +18,3 @@ concurrency:
jobs:
pr-ci:
uses: ./.github/workflows/webapp-ci-template.yml
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

11
.github/workflows/webapp-ci-template.yml поставляемый
Просмотреть файл

@@ -4,9 +4,6 @@
name: Web App CI Template
on:
workflow_call:
secrets:
CODECOV_TOKEN:
required: true
jobs:
check-lint:
@@ -78,14 +75,6 @@ jobs:
NODE_OPTIONS: --max_old_space_size=5120
run: |
npm run test-ci
- name: Upload coverage to Codecov
# Skip coverage upload for cherry-pick PRs into release branches.
if: ${{ github.event_name != 'pull_request' || !startsWith(github.event.pull_request.base.ref, 'release-') }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
disable_search: true
files: ./webapp/channels/coverage/lcov.info
build:
runs-on: ubuntu-22.04

13
.gitignore поставляемый
Просмотреть файл

@@ -40,9 +40,9 @@ e2e-tests/playwright/playwright-report
e2e-tests/playwright/storage_state
e2e-tests/playwright/test-results
e2e-tests/playwright/results
e2e-tests/playwright/specs/**/*-darwin.png
e2e-tests/playwright/specs/**/*-window.png
e2e-tests/playwright/specs/accessibility/**/*-snapshots
e2e-tests/playwright/tests/**/*-darwin.png
e2e-tests/playwright/tests/**/*-window.png
e2e-tests/playwright/tests/accessibility/**/*-snapshots
e2e-tests/playwright/.eslintcache
# ignore temporary added configuration for pgloader
@@ -159,10 +159,3 @@ docker-compose.override.yaml
.notice-work/
.aider*
.env
**/CLAUDE.local.md
CLAUDE.md
.cursorrules
server/prev-report.xml
server/prev-gotestsum.json
server/shard-*.txt

Просмотреть файл

@@ -7,8 +7,6 @@ stages:
variables:
DEBIAN_FRONTEND: noninteractive
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/usr/local/go/bin
GO_VERSION: 1.25.8
GO_HASHSUM: ceb5e041bbc3893846bd1614d76cb4681c91dadee579426cf21a63f2d7e03be6
build:
stage: build
@@ -18,13 +16,13 @@ build:
- apt install -qq -y build-essential libpng-dev libpng16-16 wget curl git
- ulimit -n 8096
- cd /tmp
- wget -q "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz"
- wget -q "https://go.dev/dl/go1.22.3.linux-amd64.tar.gz"
- >
echo \
"${GO_HASHSUM} go${GO_VERSION}.linux-amd64.tar.gz" \
> "go${GO_VERSION}.linux-amd64.tar.gz.sha256sum"
- sha256sum -c "go${GO_VERSION}.linux-amd64.tar.gz.sha256sum"
- tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz"
"8920ea521bad8f6b7bc377b4824982e011c19af27df88a815e3586ea895f1b36 go1.22.3.linux-amd64.tar.gz" \
> go1.22.3.linux-amd64.tar.gz.sha256sum
- sha256sum -c go1.22.3.linux-amd64.tar.gz.sha256sum
- tar -C /usr/local -xzf go1.22.3.linux-amd64.tar.gz
- cd -
script:
- >

Просмотреть файл

@@ -2,6 +2,4 @@
Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](https://developers.mattermost.com/contribute/getting-started/) which describes the process for making code contributions across Mattermost projects and [join our "Contributors" community channel](https://community.mattermost.com/core/channels/tickets) to ask questions from community members and the Mattermost core team.
In addition, we recommend reviewing the [Contribution Guidelines](https://handbook.mattermost.com/contributors/contributors/guidelines/contribution-guidelines) in the Mattermost Handbook, which provide comprehensive best practices and expectations for contributors.
When you submit a pull request, it goes through a [code review process outlined here](https://developers.mattermost.com/contribute/getting-started/code-review/).

Просмотреть файл

@@ -2971,64 +2971,6 @@ AWS SDK for the Go programming language.
limitations under the License.
---
## axios
This product contains 'axios' by Matt Zabriskie.
Promise based HTTP client for the browser and node.js
* HOMEPAGE:
* https://axios-http.com
* LICENSE: MIT
# Copyright (c) 2014-present Matt Zabriskie & Collaborators
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## bep/imagemeta
This product contains 'bep/imagemeta' by Bjørn Erik Pedersen.
Go library for reading EXIF, IPTC and XMP image meta data from JPEG, TIFF, PNG, and WebP files.
* HOMEPAGE:
* https://github.com/bep/imagemeta
* LICENSE: MIT License
MIT License
Copyright (c) 2022 Bjørn Erik Pedersen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## blang/semver
@@ -3437,6 +3379,41 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## code.sajari.com/docconv
This product contains 'code.sajari.com/docconv' by Search.io.
Converts PDF, DOC, DOCX, XML, HTML, RTF, etc to plain text
* HOMEPAGE:
* https://github.com/sajari/docconv
* LICENSE: MIT License
The MIT License (MIT)
Copyright (c) 2014 Sajari Pty Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## color-contrast-checker
@@ -3949,6 +3926,16 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## dynamic-virtualized-list
This product contains 'dynamic-virtualized-list'.
security holding package
---
## elastic/go-elasticsearch
@@ -9067,14 +9054,14 @@ SOFTWARE.
---
## mholt/archives
## mholt/archiver
This product contains 'mholt/archives' by Matt Holt.
This product contains 'mholt/archiver' by Matt Holt.
Cross-platform library to create & extract archives, compress & decompress files, and walk virtual file systems across various formats
Easily create & extract archives, and compress & decompress files of various formats
* HOMEPAGE:
* https://pkg.go.dev/github.com/mholt/archives
* https://pkg.go.dev/github.com/mholt/archiver/v4
* LICENSE: MIT License
@@ -9403,27 +9390,27 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## monaco-editor
## oov/psd
This product contains 'monaco-editor'.
This product contains 'psd' by oov.
A browser based code editor
A PSD/PSB file reader for go
* HOMEPAGE:
* https://github.com/microsoft/monaco-editor
* https://github.com/oov/psd
* LICENSE: MIT
The MIT License (MIT)
MIT License
Copyright (c) 2016 - present Microsoft Corporation
Copyright (c) 2016 oov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
@@ -9436,43 +9423,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## monaco-editor-webpack-plugin
This product contains 'monaco-editor-webpack-plugin' by Microsoft Corporation.
A webpack plugin for the Monaco Editor
* HOMEPAGE:
* https://github.com/microsoft/monaco-editor#readme
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2016 - present Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## opensearch-project/opensearch-go
@@ -9933,6 +9883,21 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## popper.js
This product contains 'popper.js' by Federico Zivolo.
A kickass library to manage your poppers
* HOMEPAGE:
* https://popper.js.org/
* LICENSE: MIT
---
## process
@@ -10695,6 +10660,42 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-hot-loader
This product contains 'react-hot-loader' by Dan Abramov.
Tweak React components in real time.
* HOMEPAGE:
* https://github.com/gaearon/react-hot-loader
* LICENSE: MIT
MIT License
Copyright (c) 2016 Dan Abramov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-intl
@@ -10798,6 +10799,42 @@ SOFTWARE.
---
## react-popper
This product contains 'react-popper' by Travis Arnold.
Official library to use Popper on React projects
* HOMEPAGE:
* https://popper.js.org/react-popper
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2018 React Popper authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-redux
@@ -11567,6 +11604,44 @@ RudderStack is an open-source, warehouse-first Customer Data Pipeline and Segmen
---
## rwcarlsen/goexif
This product contains 'goexif' by Robert Carlsen.
Decode embedded EXIF meta data from image files.
* HOMEPAGE:
* https://github.com/rwcarlsen/goexif
* LICENSE: BSD-2-Clause
Copyright (c) 2012, Robert Carlsen & Contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## semver
@@ -12896,3 +12971,5 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Просмотреть файл

@@ -1,6 +1,6 @@
![](mostlymatter/logo_with_white_background-small.png) Mostlymatter is a fork of [Mattermost](https://mattermost.com) meant to remove the users and messages limits.
[Mattermost](https://mattermost.com) is an open core, self-hosted collaboration platform that offers chat, workflow automation, voice calling, screen sharing, and AI integration. This repo is the primary source for core development on the Mattermost platform; it's written in Go and React, runs as a single Linux binary, and relies on PostgreSQL. A new compiled version is released under an MIT license every month on the 16th.
[Mattermost](https://mattermost.com) is an open source platform for secure collaboration across the entire software development lifecycle. Mattermost is written in Go and React and runs as a single Linux binary with MySQL or PostgreSQL.
Please go to <https://github.com/mattermost/mattermost/> for installation instructions.
@@ -18,11 +18,18 @@ The modifications are contained in the file [`limitless.patch`](limitless.patch)
To apply our modifications and compile Mostlymatter, please have a look at [MOSTLYMATTER_HOW_TO.md](MOSTLYMATTER_HOW_TO.md).
**Important**: Framasoft has not time or competences to create or maintain a fork heavier than that.
We cant keep the SSO working, for exemple, as asked in [issue #4](https://framagit.org/framasoft/framateam/mostlymatter/-/issues/4) and cant accept contributions for that, as asked in [issue #10](https://framagit.org/framasoft/framateam/mostlymatter/-/issues/10).
We don't have the necessary competences neither in Go or SSO to make sure things are done properly, and we want to keep following upstream new versions fast and simple.
If youre working on a SSO-capable fork, please let us now so we can point people to it 🙂
## Get Mostlymatter binaries
Go to <https://packages.framasoft.org/projects/mostlymatter/> to get the binaries you want.
Follow the instructions on top of the page to verify the binaries you downloaded.
Follow the instructions on top of the page to verify and install the binaries you downloaded.
## License

Просмотреть файл

@@ -59,15 +59,13 @@ build-v4: node_modules playbooks
@cat $(V4_SRC)/metrics.yaml >> $(V4_YAML)
@cat $(V4_SRC)/scheduled_post.yaml >> $(V4_YAML)
@cat $(V4_SRC)/custom_profile_attributes.yaml >> $(V4_YAML)
@cat $(V4_SRC)/audit_logging.yaml >> $(V4_YAML)
@cat $(V4_SRC)/access_control.yaml >> $(V4_YAML)
@if [ -r $(PLAYBOOKS_SRC)/paths.yaml ]; then cat $(PLAYBOOKS_SRC)/paths.yaml >> $(V4_YAML); fi
@if [ -r $(PLAYBOOKS_SRC)/merged-definitions.yaml ]; then cat $(PLAYBOOKS_SRC)/merged-definitions.yaml >> $(V4_YAML); else cat $(V4_SRC)/definitions.yaml >> $(V4_YAML); fi
@echo Extracting code samples
cd server && go run . $(V4_YAML)
@node_modules/.bin/swagger-cli validate $(V4_YAML)
@cp ./v4/html/ssr_template.hbs ./v4/html/index.html
@node_modules/.bin/redocly -t ./v4/html/ssr_template.hbs build-docs ./v4/html/static/mattermost-openapi-v4.yaml -o ./v4/html/index.html
@echo Complete
node_modules: package.json $(wildcard package-lock.json)
@@ -77,8 +75,9 @@ node_modules: package.json $(wildcard package-lock.json)
touch $@
run:
@echo Starting local server
python3 -m http.server 8080 --directory ./v4/html
@echo Starting redoc server
@node_modules/.bin/redocly preview-docs ./v4/html/static/mattermost-openapi-v4.yaml
clean:
@echo Cleaning

Просмотреть файл

@@ -1,6 +1,6 @@
# Mattermost API Documentation
This repository holds the API reference documentation for Mattermost available at [https://developers.mattermost.com/api-reference](https://developers.mattermost.com/api-reference).
This repository holds the API reference available at [https://api.mattermost.com](https://api.mattermost.com).
The Mattermost API reference uses the [OpenAPI standard](https://openapis.org/) and the [ReDoc document generator](https://github.com/Rebilly/ReDoc).
@@ -25,4 +25,4 @@ To test locally, run `make build`, `make run` and navigate to `http://127.0.0.1:
## Deployment
Deployment is handled automatically by our Github Actions. When a pull request is merged, it will automatically be deployed to [https://developers.mattermost.com/api-reference](https://developers.mattermost.com/api-reference).
Deployment is handled automatically by our Jenkins CLI machine. When a pull request is merged it will automatically be deployed to [https://api.mattermost.com](https://api.mattermost.com).

Просмотреть файл

@@ -1,7 +1,7 @@
{
"name": "mattermost-api-reference",
"version": "1.0.0",
"description": "This repository holds the API reference documentation for Mattermost available at https://developers.mattermost.com/api-reference",
"description": "This repository holds the API reference available at [https://api.mattermost.com](https://api.mattermost.com).",
"main": "index.js",
"dependencies": {
"@redocly/cli": "^1.13.0",
@@ -28,5 +28,6 @@
"license": "ISC",
"bugs": {
"url": "https://github.com/mattermost/mattermost/issues"
}
},
"homepage": "https://api.mattermost.com/"
}

Просмотреть файл

@@ -4,25 +4,27 @@
<head>
<meta charset="utf8" />
<title>Mattermost API Reference</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="./static/favicon.ico">
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
<style>
body {
padding: 0;
margin: 0;
height: 100vh;
}
label[type="tag"] {
text-transform: uppercase;
}
h1{
text-transform: capitalize;
}
</style>
{{{redocHead}}}
{{#unless disableGoogleFont}}<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">{{/unless}}
</head>
<body>
<elements-api
apiDescriptionUrl="./static/mattermost-openapi-v4.yaml"
router="hash"
layout="sidebar"
/>
{{{redocHTML}}}
</body>
</html>

Просмотреть файл

@@ -1,534 +0,0 @@
/api/v4/access_control_policies:
put:
tags:
- access control
summary: Create an access control policy
description: |
Creates a new access control policy.
##### Permissions
Must have the `manage_system` permission.
operationId: CreateAccessControlPolicy
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AccessControlPolicy"
responses:
"200":
description: Access control policy created successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/AccessControlPolicy"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/access_control_policies/cel/check:
post:
tags:
- access control
summary: Check an access control policy expression
description: |
Checks the syntax and validity of an access control policy expression.
##### Permissions
Must have the `manage_system` permission.
operationId: CheckAccessControlPolicyExpression
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
expression:
type: string
description: The expression to check.
responses:
"200":
description: Expression check result.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ExpressionError"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/access_control_policies/cel/test:
post:
tags:
- access control
summary: Test an access control policy expression
description: |
Tests an access control policy expression against users to see who would be affected.
##### Permissions
Must have the `manage_system` permission.
operationId: TestAccessControlPolicyExpression
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/QueryExpressionParams"
responses:
"200":
description: Expression test result.
content:
application/json:
schema:
$ref: "#/components/schemas/AccessControlPolicyTestResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/access_control_policies/search:
post:
tags:
- access control
summary: Search access control policies
description: |
Searches for access control policies based on given criteria.
##### Permissions
Must have the `manage_system` permission.
operationId: SearchAccessControlPolicies
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AccessControlPolicySearch"
responses:
"200":
description: Search results for access control policies.
content:
application/json:
schema:
$ref: "#/components/schemas/AccessControlPoliciesWithCount"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/access_control_policies/cel/autocomplete/fields:
get:
tags:
- access control
summary: Get autocomplete fields for access control policies
description: |
Provides a list of fields that can be used for autocompletion when creating/editing access control policy expressions.
##### Permissions
Must have the `manage_system` permission.
operationId: GetAccessControlPolicyAutocompleteFields
parameters:
- name: after
in: query
description: The field ID to start after for pagination.
required: false
schema:
type: string
- name: limit
in: query
description: The maximum number of fields to return.
required: true
schema:
type: integer
default: 60
responses:
"200":
description: Autocomplete fields retrieved successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/AccessControlFieldsAutocompleteResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
"/api/v4/access_control_policies/{policy_id}":
get:
tags:
- access control
summary: Get an access control policy
description: |
Gets a specific access control policy by its ID.
##### Permissions
Must have the `manage_system` permission.
operationId: GetAccessControlPolicy
parameters:
- name: policy_id
in: path
description: The ID of the access control policy.
required: true
schema:
type: string
responses:
"200":
description: Access control policy retrieved successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/AccessControlPolicy"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
delete:
tags:
- access control
summary: Delete an access control policy
description: |
Deletes an access control policy by its ID.
##### Permissions
Must have the `manage_system` permission.
operationId: DeleteAccessControlPolicy
parameters:
- name: policy_id
in: path
description: The ID of the access control policy.
required: true
schema:
type: string
responses:
"200":
description: Access control policy deleted successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
"/api/v4/access_control_policies/{policy_id}/activate":
get:
tags:
- access control
summary: Activate or deactivate an access control policy
deprecated: true
description: |
This endpoint is deprecated. Use the POST /api/v4/access_control_policies/{policy_id}/activate instead.
responses:
"405":
$ref: "#/components/responses/MethodNotAllowed"
post:
tags:
- access control
summary: Activate or deactivate an access control policy
description: |
Updates the active status of an access control policy.
##### Permissions
Must have the `manage_system` permission.
operationId: UpdateAccessControlPolicyActiveStatus
parameters:
- name: policy_id
in: path
description: The ID of the access control policy.
required: true
schema:
type: string
- name: active
in: query
description: Set to "true" to activate, "false" to deactivate.
required: true
schema:
type: boolean
responses:
"200":
description: Policy active status updated successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
"/api/v4/access_control_policies/{policy_id}/assign":
post:
tags:
- access control
summary: Assign an access control policy to channels
description: |
Assigns an access control policy to a list of channels.
##### Permissions
Must have the `manage_system` permission.
operationId: AssignAccessControlPolicyToChannels
parameters:
- name: policy_id
in: path
description: The ID of the access control policy.
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
channel_ids:
type: array
items:
type: string
description: The IDs of the channels to assign the policy to.
responses:
"200":
description: Policy assigned to channels successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
"/api/v4/access_control_policies/{policy_id}/unassign":
delete:
tags:
- access control
summary: Unassign an access control policy from channels
description: |
Unassigns an access control policy from a list of channels.
##### Permissions
Must have the `manage_system` permission.
operationId: UnassignAccessControlPolicyFromChannels
parameters:
- name: policy_id
in: path
description: The ID of the access control policy.
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
channel_ids:
type: array
items:
type: string
description: The IDs of the channels to unassign the policy from.
responses:
"200":
description: Policy unassigned from channels successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
"/api/v4/access_control_policies/{policy_id}/resources/channels":
get:
tags:
- access control
summary: Get channels for an access control policy
description: |
Retrieves a paginated list of channels to which a specific access control policy is applied.
##### Permissions
Must have the `manage_system` permission.
operationId: GetChannelsForAccessControlPolicy
parameters:
- name: policy_id
in: path
description: The ID of the access control policy.
required: true
schema:
type: string
- name: after
in: query
description: The channel ID to start after for pagination.
required: false
schema:
type: string
- name: limit
in: query
description: The maximum number of channels to return.
required: true
schema:
type: integer
default: 60
responses:
"200":
description: Channels retrieved successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/ChannelsWithCount"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
"/api/v4/access_control_policies/{policy_id}/resources/channels/search":
post:
tags:
- access control
summary: Search channels for an access control policy
description: |
Searches for channels associated with a specific access control policy based on search criteria.
##### Permissions
Must have the `manage_system` permission.
operationId: SearchChannelsForAccessControlPolicy
parameters:
- name: policy_id
in: path
description: The ID of the access control policy.
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ChannelSearch"
responses:
"200":
description: Channel search results retrieved successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/ChannelsWithCount"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
"/api/v4/channels/{channel_id}/access_control/attributes":
get:
tags:
- access control
- channels
summary: Get access control attributes for a channel
description: |
Retrieves the effective access control policy attributes for a specific channel.
This can be used to understand what attributes are currently being applied to the channel by the access control system.
##### Permissions
Must have `read_channel` permission for the specified channel.
operationId: GetChannelAccessControlAttributes
parameters:
- name: channel_id
in: path
description: The ID of the channel.
required: true
schema:
type: string
responses:
"200":
description: Access control attributes retrieved successfully.
content:
application/json:
schema:
type: object # Placeholder - define more specifically if the structure is known
additionalProperties: true
description: A map of attribute names to their values as applied to the channel.
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/access_control_policies/cel/visual_ast:
post:
tags:
- access control
summary: Get the visual AST for a CEL expression
description: |
Retrieves the visual AST for a CEL expression.
##### Permissions
Must have the `manage_system` permission.
operationId: GetCELVisualAST
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CELExpression"
responses:
"200":
description: Visual AST retrieved successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/VisualExpression"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"

Просмотреть файл

@@ -1,68 +0,0 @@
/api/v4/audit_logs/certificate:
post:
tags:
- audit_logs
summary: Upload audit log certificate
description: |
Upload the certificate to be used for TLS verification with the audit log service.
##### Permissions
Must have `sysconsole_write_experimental_features` permission.
__Minimum server version__: 10.9
operationId: AddAuditLogCertificate
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
certificate:
description: The certificate file
type: string
format: binary
required:
- certificate
responses:
"200":
description: Certificate upload successful
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"413":
$ref: "#/components/responses/TooLarge"
"501":
$ref: "#/components/responses/NotImplemented"
delete:
tags:
- audit_logs
summary: Remove audit log certificate
description: |
Delete the current certificate being used with the audit log service.
##### Permissions
Must have `sysconsole_write_experimental_features` permission.
__Minimum server version__: 9.5
operationId: RemoveAuditLogCertificate
responses:
"200":
description: Certificate deletion successful
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"501":
$ref: "#/components/responses/NotImplemented"

Просмотреть файл

@@ -641,8 +641,6 @@
type: string
description: Markdown-formatted text to display in the header of the
channel
banner_info:
$ref: "#/components/schemas/ChannelBanner"
description: Channel object to be updated
required: true
responses:
@@ -2517,41 +2515,3 @@
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"/api/v4/sharedchannels/{channel_id}/remotes":
get:
tags:
- channels
summary: Get remote clusters for a shared channel
description: |
Gets the remote clusters information for a shared channel.
__Minimum server version__: 10.10
##### Permissions
Must be authenticated and have the `read_channel` permission for the channel.
operationId: GetSharedChannelRemotes
parameters:
- name: channel_id
in: path
description: Channel GUID
required: true
schema:
type: string
responses:
"200":
description: Remote clusters retrieval successful
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/RemoteClusterInfo"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"

Просмотреть файл

@@ -53,36 +53,7 @@
type:
type: string
attrs:
type: object
properties:
visibility:
type: string
description: "Visibility of the attribute"
enum: ["hidden", "when_set", "always"]
default: "when_set"
sort_order:
type: number
description: "Sort order for displaying this attribute"
options:
type: array
description: "Options for select/multiselect fields"
items:
type: object
properties:
name:
type: string
color:
type: string
value_type:
type: string
description: "Type of text value"
enum: ["email", "url", "phone"]
ldap:
type: string
description: "LDAP attribute for syncing"
saml:
type: string
description: "SAML attribute for syncing"
type: string
responses:
"201":
description: Custom Profile Attribute field creation successful
@@ -135,38 +106,7 @@
type:
type: string
attrs:
type: object
properties:
visibility:
type: string
description: "Visibility of the attribute"
enum: ["hidden", "when_set", "always"]
default: "when_set"
sort_order:
type: number
description: "Sort order for displaying this attribute"
options:
type: array
description: "Options for select/multiselect fields"
items:
type: object
properties:
id:
type: string
name:
type: string
color:
type: string
value_type:
type: string
description: "Type of text value"
enum: ["email", "url", "phone"]
ldap:
type: string
description: "LDAP attribute for syncing"
saml:
type: string
description: "SAML attribute for syncing"
type: string
responses:
"200":
description: Custom Profile Attribute field patch successful
@@ -278,35 +218,6 @@
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"/api/v4/custom_profile_attributes/group":
get:
tags:
- custom profile attributes
summary: Get Custom Profile Attribute property group data
description: |
Get the property group used for Custom Profile Attributes.
__Minimum server version__: 10.8
##### Permissions
Must be authenticated.
operationId: GetCPAGroup
responses:
"200":
description: Group fetch successful
content:
application/json:
schema:
type: object
properties:
id:
type: string
description: The ID of the custom profile attributes group
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"/api/v4/users/{user_id}/custom_profile_attributes":
get:
tags:

Просмотреть файл

@@ -1,8 +1,9 @@
components:
securitySchemes:
bearerAuth:
bearerAuth: # arbitrary name for the security scheme
type: http
scheme: bearer
bearerFormat: Token
responses:
Forbidden:
description: Do not have appropriate permissions
@@ -28,12 +29,6 @@ components:
application/json:
schema:
$ref: "#/components/schemas/AppError"
MethodNotAllowed:
description: Method not allowed
content:
application/json:
schema:
$ref: "#/components/schemas/AppError"
TooLarge:
description: Content too large
content:
@@ -180,6 +175,8 @@ components:
type: string
total_member_count:
type: integer
active_member_count:
type: integer
TeamExists:
type: object
properties:
@@ -1286,131 +1283,6 @@ components:
type: string
session_id:
type: string
LdapSettings:
type: object
properties:
Enable:
type: boolean
EnableSync:
type: boolean
LdapServer:
type: string
LdapPort:
type: integer
ConnectionSecurity:
type: string
BaseDN:
type: string
BindUsername:
type: string
BindPassword:
type: string
MaximumLoginAttempts:
type: integer
UserFilter:
type: string
GroupFilter:
type: string
GuestFilter:
type: string
EnableAdminFilter:
type: boolean
AdminFilter:
type: string
GroupDisplayNameAttribute:
type: string
GroupIdAttribute:
type: string
FirstNameAttribute:
type: string
LastNameAttribute:
type: string
EmailAttribute:
type: string
UsernameAttribute:
type: string
NicknameAttribute:
type: string
IdAttribute:
type: string
PositionAttribute:
type: string
LoginIdAttribute:
type: string
PictureAttribute:
type: string
SyncIntervalMinutes:
type: integer
ReAddRemovedMembers:
type: boolean
SkipCertificateVerification:
type: boolean
PublicCertificateFile:
type: string
PrivateKeyFile:
type: string
QueryTimeout:
type: integer
MaxPageSize:
type: integer
LoginFieldName:
type: string
LoginButtonColor:
type: string
LoginButtonBorderColor:
type: string
LoginButtonTextColor:
type: string
LdapDiagnosticResult:
type: object
properties:
test_name:
type: string
description: Name/type of the diagnostic test being performed
test_value:
type: string
description: The actual test value (filter string or attribute name)
total_count:
type: integer
description: Number of entries found by the filter
message:
type: string
description: Optional success/info message
error:
type: string
description: Optional error message if test failed
sample_results:
type: array
description: Array of sample LDAP entries found
items:
type: object
properties:
dn:
type: string
description: Distinguished Name
username:
type: string
description: Username
email:
type: string
description: Email
first_name:
type: string
description: First name
last_name:
type: string
description: Last name
id:
type: string
description: ID attribute
display_name:
type: string
description: Display name for groups
available_attributes:
type: object
description: Map of all available LDAP attributes
additionalProperties:
type: string
Config:
type: object
properties:
@@ -1702,12 +1574,6 @@ components:
type: string
ReportAProblemLink:
type: string
ReportAProblemType:
type: string
ReportAProblemMail:
type: string
AllowDownloadLogs:
type: boolean
SupportEmail:
type: string
GitLabSettings:
@@ -2182,12 +2048,6 @@ components:
type: boolean
ReportAProblemLink:
type: boolean
ReportAProblemType:
type: boolean
ReportAProblemMail:
type: boolean
AllowDownloadLogs:
type: boolean
SupportEmail:
type: boolean
GitLabSettings:
@@ -4064,193 +3924,6 @@ components:
description: Explains the error behind why a scheduled post could not have been sent
metadata:
$ref: "#/components/schemas/PostMetadata"
AccessControlFieldsAutocompleteResponse:
type: object
properties:
fields:
type: array
items:
type: object
properties:
name:
type: string
description: The name of the field.
description:
type: string
description: A description of the field.
AccessControlPoliciesWithCount:
type: object
properties:
policies:
type: array
items:
$ref: "#/components/schemas/AccessControlPolicy"
total_count:
type: integer
description: The total number of policies.
AccessControlPolicy:
type: object
properties:
id:
type: string
description: The unique identifier of the policy.
name:
type: string
description: The unique name for the policy.
display_name:
type: string
description: The human-readable name for the policy.
description:
type: string
description: A description of the policy.
expression:
type: string
description: The CEL expression defining the policy rules.
is_active:
type: boolean
description: Whether the policy is currently active and enforced.
create_at:
type: integer
format: int64
description: The time in milliseconds the policy was created.
update_at:
type: integer
format: int64
description: The time in milliseconds the policy was last updated.
delete_at:
type: integer
format: int64
description: The time in milliseconds the policy was deleted.
AccessControlPolicySearch:
type: object
properties:
term:
type: string
description: The search term to match against policy names or display names.
is_active:
type: boolean
description: Filter policies by active status.
page:
type: integer
description: The page number to return.
per_page:
type: integer
description: The number of policies to return per page.
# Add other potential search/filter fields like sort_by, sort_direction
AccessControlPolicyTestResponse:
type: object
properties:
users:
type: array
items:
$ref: "#/components/schemas/User"
description: A list of users affected by the policy expression.
total_count:
type: integer
description: The total number of users affected.
ChannelSearch: # Added based on dataretention.yaml and access_control.go usage
type: object
properties:
term:
type: string
description: The string to search in the channel name, display name, and purpose.
team_ids:
type: array
items:
type: string
description: Filters results to channels belonging to the given team ids.
public:
type: boolean
description: Filters results to only return Public / Open channels.
private:
type: boolean
description: Filters results to only return Private channels.
deleted:
type: boolean
description: Filters results to only return deleted / archived channels.
include_deleted:
type: boolean
description: Whether to include deleted channels in the search results.
# Add other potential search fields like not_associated_to_group, exclude_default_channels etc.
ChannelsWithCount: # Added based on access_control.go usage
type: object
properties:
channels:
$ref: "#/components/schemas/ChannelListWithTeamData" # Referencing existing type used in similar contexts
total_count:
type: integer
description: The total number of channels.
ExpressionError:
type: object
properties:
message:
type: string
description: The error message.
field:
type: string
description: The field related to the error, if applicable.
line:
type: integer
description: The line number where the error occurred in the expression.
column:
type: integer
description: The column number where the error occurred in the expression.
QueryExpressionParams:
type: object
properties:
expression:
type: string
description: The policy expression to test.
term:
type: string
description: A search term to filter users against whom the expression is tested.
limit:
type: integer
description: The maximum number of users to return.
after:
type: string
description: The ID of the user to start the test after (for pagination).
CELExpression:
type: object
properties:
expression:
type: string
description: The CEL expression to visualize.
VisualExpression:
type: object
properties:
conditions:
type: array
items:
$ref: "#/components/schemas/Condition"
description: The visual AST for the CEL expression
Condition:
type: object
properties:
attribute:
type: string
description: The attribute name.
operator:
type: string
description: The operator of a single condition.
value:
type: string
description: The value.
value_type:
type: string
description: The value type.
ChannelBanner:
type: object
properties:
enabled:
type: boolean
description: enabled indicates whether the channel banner is enabled or not
text:
type: string
description: text is the actual text that renders in the channel banner. Markdown is supported.
background_color:
type: string
description: background_color is the HEX color code for the banner's backgroubd
externalDocs:
description: Find out more about Mattermost
url: 'https://about.mattermost.com'

Просмотреть файл

@@ -8,6 +8,9 @@
team.
`not_associated_to_team` **OR** `not_associated_to_channel` is required.
If you use `not_associated_to_team`, you must be a team admin for that particular team (permission to manage that team).
@@ -46,12 +49,14 @@
in: query
description: Team GUID which is used to return all the groups not associated to
this team
required: true
schema:
type: string
- name: not_associated_to_channel
in: query
description: Group GUID which is used to return all the groups not associated to
this channel
required: true
schema:
type: string
- name: since
@@ -1035,59 +1040,6 @@
schema:
type: boolean
default: false
- name: include_member_count
in: query
description: Boolean which adds a `member_count` field to each group object.
schema:
type: boolean
default: false
- name: include_timezones
in: query
description: Boolean which adds timezone information for group members.
schema:
type: boolean
default: false
- name: include_total_count
in: query
description: Boolean which adds total count of groups in the response.
schema:
type: boolean
default: false
- name: include_archived
in: query
description: Boolean which includes archived groups in the response.
schema:
type: boolean
default: false
- name: filter_archived
in: query
description: Boolean which filters out archived groups from the response.
schema:
type: boolean
default: false
- name: filter_parent_team_permitted
in: query
description: Boolean which filters groups based on parent team permissions.
schema:
type: boolean
default: false
- name: filter_has_member
in: query
description: User ID to filter groups that have this member.
schema:
type: string
- name: include_member_ids
in: query
description: Boolean which adds member IDs to the group objects.
schema:
type: boolean
default: false
- name: only_syncable_sources
in: query
description: Boolean which includes groups from syncable sources.
schema:
type: boolean
default: false
responses:
"200":
description: Group list retrieval successful

Просмотреть файл

@@ -22,34 +22,3 @@
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"/api/v4/imports/{import_name}":
delete:
tags:
- imports
summary: Delete an import file
description: |
Deletes an import file.
__Minimum server version__: 5.31
##### Permissions
Must have `manage_system` permissions.
operationId: DeleteImport
parameters:
- name: import_name
in: path
description: The name of the import file to delete
required: true
schema:
type: string
responses:
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"

Просмотреть файл

@@ -1,173 +1,184 @@
openapi: 3.0.0
info:
description: >
The Mattermost Web Services API enables Mattermost clients and third-party applications
to interact with Mattermost servers.
There is also a work-in-progress [Postman API
reference](https://documenter.getpostman.com/view/4508214/RW8FERUn).
version: 4.0.0
title: Mattermost API Reference
termsOfService: https://about.mattermost.com/default-terms/
contact:
email: feedback@mattermost.com
x-logo:
url: https://mattermost.com/wp-content/uploads/2022/02/logoHorizontal.png
backgroundColor: "#FFFFFF"
tags:
- name: introduction
description: >
The Mattermost Web Services API is used by Mattermost clients and third
party applications to interact with the server. [JavaScript and Golang
drivers for](/#tag/drivers) connecting to the APIs are also available.
[Official JavaScript and Golang drivers](#/#official-drivers)
are available to simplify API integration.
### Support
By using this API, you agree to our [terms of service](https://about.mattermost.com/default-terms/).
Mattermost core committers work with the community to keep the API documentation up-to-date.
[Find out more about Mattermost](https://about.mattermost.com/)
## Contents
### Core Concepts
* [Schema & Conventions](#/#schema--conventions)
* [Authentication](#/#authentication)
* [Rate Limiting](#/#rate-limiting)
* [Error Handling](#/#error-handling)
* [WebSocket](#/#websocket)
* [Authentication](#/#authentication-1)
* [Websocket Events](#/#websocket-events)
* [Websocket API](#/#websocket-api)
### Drivers
* [Official Drivers](#/#official-drivers)
* [Community-built Drivers](#/#community-built-drivers)
### Community
* [Support](#/#support)
* [Contributing](#/#contributing)
If you have questions on API routes not listed in this reference, please [join the Mattermost community server](https://community.mattermost.com) to ask questions in the Developers channel, [or post questions to our Developer Discussion forum](https://forum.mattermost.org/c/dev).
## Schema & Conventions
[Bug reports](https://github.com/mattermost/mattermost/issues) in the documentation or the API are also welcome, as are pull requests to fix the issues.
- All API access is through HTTP(S) requests at `your-mattermost-url/api/v4`.
- All request and response bodies are `application/json`.
### Contributing
- When using endpoints that require a user id, the string `me` can be used in place
of the user id to indicate the action is to be taken for the logged in user.
- For all endpoints that implement pagination via the `per_page` parameter:
When you have answers to API questions not addressed in our documentation we ask you to consider making a pull request to improve our reference. Small and large changes are all welcome.
- Maximum items per page: 200 (requests exceeding this will be silently truncated)
- Default value if a paged API requires a `per_page` parameter and it is not provided: 60
We also have [Help Wanted tickets](https://github.com/mattermost/mattermost/issues?q=is%3Aopen+is%3Aissue+label%3AArea%2FAPI) available for community members who would like to help others more easily use the APIs. We acknowledge everyone's contribution in the [release notes of our next version](https://docs.mattermost.com/administration/changelog.html#contributors).
## Authentication
The source code for this API reference is hosted at https://github.com/mattermost/mattermost/tree/master/api.
- name: schema
description: >
All API access is through HTTP(S) requests at
`your-mattermost-url.com/api/v4`. All request and response bodies are
`application/json`.
When using endpoints that require a user id, the string `me` can be used in place of the user id to indicate the action is to be taken for the logged in user.
For all endpoints that implement pagination via the `per_page` parameter, the maximum number of items returned per request is capped at 200. If `per_page` exceeds 200, the list will be silently truncated to 200 items.
- name: drivers
description: >
The easiest way to interact with the Mattermost Web Service API is through
a language specific driver.
#### Official Drivers
* Mattermost JavaScript/TypeScript Driver
* [NPM](https://www.npmjs.com/package/@mattermost/client)
* [Source](https://github.com/mattermost/mattermost/tree/master/webapp/platform/client)
* [Mattermost Golang Driver](https://pkg.go.dev/github.com/mattermost/mattermost/server/public/model#Client4)
#### Community-built Drivers
For community-built drivers and API wrappers, see [our app directory](https://mattermost.com/marketplace/).
- name: authentication
description: >
There are multiple ways to authenticate against the Mattermost API.
All examples assume there is a Mattermost instance running at http://localhost:8065.
### Session Token
Make an HTTP POST to `your-mattermost-url/api/v4/users/login` with a JSON
body indicating the user's `login_id`, `password` and optionally the MFA
`token`. The `login_id` can be an email, username or an AD/LDAP ID
depending on the system's configuration.
#### Session Token
Make an HTTP POST to `your-mattermost-url.com/api/v4/users/login` with a JSON body indicating the users `login_id`, `password` and optionally the MFA `token`. The `login_id` can be an email, username or an AD/LDAP ID depending on the system's configuration.
```bash
curl -i -d '{"login_id":"someone@nowhere.com","password":"thisisabadpassword"}' http://localhost:8065/api/v4/users/login
```
curl -i -d '{"login_id":"someone@nowhere.com","password":"thisisabadpassword"}' http://localhost:8065/api/v4/users/login
```
NOTE: If you're running cURL on windows, you will have to change the single quotes to double quotes, and escape the inner double quotes with backslash, like below:
```bash
curl -i -d "{\"login_id\":\"someone@nowhere.com\",\"password\":\"thisisabadpassword\"}" http://localhost:8065/api/v4/users/login
```
curl -i -d "{\"login_id\":\"someone@nowhere.com\",\"password\":\"thisisabadpassword\"}" http://localhost:8065/api/v4/users/login
```
If successful, the response will contain a `Token` header and a user object in the body.
```http
```
HTTP/1.1 200 OK
Content-Type: application/json
Permissions-Policy:
Referrer-Policy: no-referrer
Token: ckh3t4knu3fzujt76o57f5jo4w
Vary: Origin
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-Request-Id: bk3uzm335jr9tnoh4mcsybmmjr
X-Version-Id: 10.6.0.13685270376.215f100adf6ccda09afcaaa84ac4bfbd.true
Date: Fri, 28 Mar 2025 09:33:22 GMT
Content-Length: 796
Set-Cookie: MMSID=hyr5dmb1mbb49c44qmx4whniso; Path=/; Max-Age=2592000; HttpOnly
Token: hyr5dmb1mbb49c44qmx4whniso
X-Ratelimit-Limit: 10
X-Ratelimit-Remaining: 9
X-Ratelimit-Reset: 1
X-Request-Id: smda55ckcfy89b6tia58shk5fh
X-Version-Id: developer
Date: Fri, 11 Sep 2015 13:21:14 GMT
Content-Length: 657
Content-Type: application/json; charset=utf-8
{{user object as json}}
```
Include the `Token` as part of the `Authorization` header on your future API requests with the `Bearer` method.
```bash
curl -i -H 'Authorization: Bearer ckh3t4knu3fzujt76o57f5jo4w' http://localhost:8065/api/v4/users/me
```
Alternatively, include the `Token` as your `MMAUTHTOKEN` cookie value on you future API requests:
```bash
curl -i -H 'Cookie: MMAUTHTOKEN=ckh3t4knu3fzujt76o57f5jo4w' http://localhost:8065/api/v4/users/me
```
curl -i -H 'Authorization: Bearer hyr5dmb1mbb49c44qmx4whniso' http://localhost:8065/api/v4/users/me
```
You should now be able to access the API as the user you logged in as.
### Personal Access Tokens
#### Personal Access Tokens
Using [personal access tokens](https://docs.mattermost.com/developer/personal-access-tokens.html) is very similar to using a session token. The only real difference is that session tokens will expire, while personal access tokens will live until they are manually revoked by the user or an admin.
Using [personal access tokens](https://developers.mattermost.com/integrate/reference/personal-access-token/) is very similar to using a session token. The only real difference is that session tokens will expire, while personal access tokens will live until they are manually revoked by the user or an admin.
Just like session tokens, include the personal access token as part of the `Authorization` header in your requests using the `Bearer` method. Assuming our personal access token is `9xuqwrwgstrb3mzrxb83nb357a`, we could use it as shown below.
```bash
```
curl -i -H 'Authorization: Bearer 9xuqwrwgstrb3mzrxb83nb357a' http://localhost:8065/api/v4/users/me
```
## Rate Limiting
Whenever you make an HTTP request to the Mattermost API you might notice
the following headers included in the response:
```http
X-Ratelimit-Limit: 10
X-Ratelimit-Remaining: 9
X-Ratelimit-Reset: 1441983590
```
These headers are telling you your current rate limit status.
#### OAuth 2.0
| Header | Description |
|:-----------------------|:-------------------------------------------------------------------|
| X-Ratelimit-Limit | The maximum number of requests you can make per second. |
| X-Ratelimit-Remaining | The number of requests remaining in the current window. |
| X-Ratelimit-Reset | The remaining UTC epoch seconds before the rate limit resets. |
Mattermost has the ability to act as an [OAuth 2.0](https://tools.ietf.org/html/rfc6749) service provider.
If you exceed your rate limit for a window you will receive the following error in the body of the response:
```http
HTTP/1.1 429 Too Many Requests
Date: Tue, 10 Sep 2015 11:20:28 GMT
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1
The official documentation for [using your Mattermost server as an OAuth 2.0 service provider can be found here.](https://docs.mattermost.com/developer/oauth-2-0-applications.html)
limit exceeded
```
## Error Handling
For an example on how to register an OAuth 2.0 app with your Mattermost instance, please see the [Mattermost-Zapier integration documentation](https://docs.mattermost.com/integrations/zapier.html#register-zapier-as-an-oauth-2-0-application).
- name: errors
description: >
All errors will return an appropriate HTTP response code along with the
following JSON body:
```json
```
{
"id": "the.error.id",
"message": "Something went wrong", // the reason for the error
@@ -177,21 +188,75 @@ info:
}
```
- name: rate limiting
description: >
Whenever you make an HTTP request to the Mattermost API you might notice
the following headers included in the response:
## WebSocket
In addition to the HTTP RESTful web service, Mattermost also offers a WebSocket event delivery system and some API functionality.
```
To connect to the WebSocket follow the standard opening handshake as [defined by the RFC specification](https://tools.ietf.org/html/rfc6455#section-1.3) to the `/api/v4/websocket` endpoint of Mattermost.
X-Ratelimit-Limit: 10
X-Ratelimit-Remaining: 9
X-Ratelimit-Reset: 1441983590
```
### Authentication
These headers are telling you your current rate limit status.
| Header | Description |
| ------ | ----------- |
| X-Ratelimit-Limit | The maximum number of requests you can make per second. |
| X-Ratelimit-Remaining | The number of requests remaining in the current window. |
| X-Ratelimit-Reset | The remaining UTC epoch seconds before the rate limit resets. |
If you exceed your rate limit for a window you will receive the following error in the body of the response:
```
HTTP/1.1 429 Too Many Requests
Date: Tue, 10 Sep 2015 11:20:28 GMT
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1
limit exceeded
```
- name: WebSocket
description: >
In addition to the HTTP RESTful web service, Mattermost also offers a
WebSocket event delivery system and some API functionality.
To connect to the WebSocket follow the standard opening handshake as [defined by the RFC specification](https://tools.ietf.org/html/rfc6455#section-1.3) to the `/api/v4/websocket` endpoint of Mattermost.
#### Authentication
The Mattermost WebSocket can be authenticated using [the standard API authentication methods](/#tag/authentication) (by a cookie or with an explicit Authorization header) or through an authentication challenge. If you're authenticating from a browser and have logged in with the Mattermost API, your authentication cookie should already be set. This is how the Mattermost webapp authenticates with the WebSocket.
The Mattermost WebSocket can be authenticated using [the standard API authentication methods](/#/#authentication) (by a cookie or with an explicit Authorization header) or through an authentication challenge. If you're authenticating from a browser and have logged in with the Mattermost API, your authentication cookie should already be set. This is how the Mattermost webapp authenticates with the WebSocket.
To authenticate with an authentication challenge, first connect the WebSocket and then send the following JSON over the connection:
```json
```
{
"seq": 1,
"action": "authentication_challenge",
@@ -199,27 +264,37 @@ info:
"token": "mattermosttokengoeshere"
}
}
```
If successful, you will receive a standard OK response over the WebSocket connection:
```json
```
{
"status": "OK",
"seq_reply": 1
}
```
Once successfully authenticated, the server will pass a `hello` WebSocket event containing server version over the connection.
### Websocket Events
#### Events
WebSocket events are primarily used to alert the client to changes in Mattermost, such as delivering new posts or alerting the client that another user is typing in a channel.
Events on the WebSocket will have the form:
```json
```
{
"event": "hello",
"data": {
@@ -233,62 +308,112 @@ info:
},
"seq": 0
}
```
The `event` field indicates the event type, `data` contains any data relevant to the event and `broadcast` contains information about who the event was sent to. For example, the above example has `user_id` set to "ay5sq51sebfh58ktrce5ijtcwy" meaning that only the user with that ID received this event broadcast. The `omit_users` field can contain an array of user IDs that were specifically omitted from receiving the event.
The list of Mattermost WebSocket events are:
- added_to_team
- authentication_challenge
- channel_converted
- channel_created
- channel_deleted
- channel_member_updated
- channel_updated
- channel_viewed
- config_changed
- delete_team
- direct_added
- emoji_added
- ephemeral_message
- group_added
- hello
- leave_team
- license_changed
- memberrole_updated
- new_user
- plugin_disabled
- plugin_enabled
- plugin_statuses_changed
- post_deleted
- post_edited
- post_unread
- posted
- preference_changed
- preferences_changed
- preferences_deleted
- reaction_added
- reaction_removed
- response
- role_updated
- status_change
- typing
- update_team
- user_added
- user_removed
- user_role_updated
- user_updated
- dialog_opened
- thread_updated
- thread_follow_changed
- thread_read_changed
### Websocket API
#### WebSocket API
Mattermost has some basic support for WebSocket APIs. A connected WebSocket can make requests by sending the following over the connection:
```json
```
{
"action": "user_typing",
"seq": 2,
@@ -297,28 +422,37 @@ info:
"parent_id": ""
}
}
```
This is an example of making a `user_typing` request, with the purpose of alerting the server that the connected client has begun typing in a channel or thread. The `action` field indicates what is being requested, and performs a similar duty as the route in a HTTP API. The `data` field is used to add any additional data along with the request. The server supports binary websocket messages as well in case the client has such a requirement.
The `seq` or sequence number is set by the client and should be incremented with every use. It is used to distinguish responses to requests that come down the WebSocket. For example, a standard response to the above request would be:
```json
```
{
"status": "OK",
"seq_reply": 2
}
```
Notice `seq_reply` is 2, matching the `seq` of the original request. Using this a client can distinguish which request the response is meant for.
If there was any information to respond with, it would be encapsulated in a `data` field.
In the case of an error, the response would be:
```json
```
{
"status": "FAIL",
"seq_reply": 2,
@@ -327,65 +461,32 @@ info:
"message": "Some error message here"
}
}
```
The list of WebSocket API actions is:
- user_typing
- get_statuses
- get_statuses_by_ids
To see how these actions work, please refer to either the [Golang WebSocket driver](https://github.com/mattermost/mattermost/blob/master/server/public/model/websocket_client.go) or our [JavaScript WebSocket driver](https://github.com/mattermost/mattermost/blob/master/webapp/platform/client/src/websocket.ts).
- name: common parameters
description: >
- `per_page`: For paged APIs, the number of items to return per page.
## Drivers
The easiest way to interact with the Mattermost Web Service API is through
a language specific driver.
The maximum number of items returned per request is capped at 200. If `per_page` exceeds 200, the list will be silently truncated to 200 items.
### Official Drivers
* Mattermost JavaScript/TypeScript Driver
* [NPM](https://www.npmjs.com/package/@mattermost/client)
* [Source](https://github.com/mattermost/mattermost/tree/master/webapp/platform/client)
* [Mattermost Golang Driver](https://pkg.go.dev/github.com/mattermost/mattermost/server/public/model#Client4)
### Community-built Drivers
For community-built drivers and API wrappers, see [our app directory](https://mattermost.com/marketplace-category/mattermost-developers-tools/).
## Support
Mattermost core committers work with the community to keep the API documentation up-to-date.
If you have questions on API routes not listed in this reference, you can:
- [Join the Mattermost community server](https://community.mattermost.com) to ask questions in the Developers channel
- Contact us at feedback@mattermost.com
[Bug reports](https://github.com/mattermost/mattermost/issues) in the documentation or the API are also welcome, as are pull requests to fix the issues.
## Contributing
When you have answers to API questions not addressed in our documentation we ask you to consider making a pull request to improve our reference. Small and large changes are all welcome.
We also have [Help Wanted tickets](https://github.com/mattermost/mattermost/issues?q=is%3Aopen+is%3Aissue+label%3AArea%2FAPI) available for community members who would like to help others more easily use the APIs. We acknowledge everyone's contribution in the [release notes of our next version](https://docs.mattermost.com/administration/changelog.html#contributors).
The source code for this API reference is hosted at https://github.com/mattermost/mattermost/tree/master/api.
version: 4.0.0
title: Mattermost API
tags:
If a paged API requires a `per_page` parameter and it is not provided, the default value is 60.
- name: users
description: >
Endpoints for creating, getting and interacting with users.
When using endpoints that require a user id, the string `me` can be used in place of the user id to indicate the action is to be taken for the logged in user.
- name: bots
description: Endpoints for creating, getting and updating bot users.
@@ -460,11 +561,64 @@ tags:
description: Endpoints related to export files.
- name: metrics
description: Endpoints related to metrics, including the Client Performance Monitoring feature.
- name: audit_logs
description: Endpoints for managing audit log certificates and configuration.
x-tagGroups:
- name: Overview
tags:
- introduction
- schema
- name: Standard Features
tags:
- drivers
- authentication
- errors
- rate limiting
- WebSocket
- common parameters
- name: Endpoints
tags:
- users
- bots
- teams
- channels
- posts
- threads
- files
- uploads
- bookmarks
- preferences
- status
- emoji
- reactions
- webhooks
- commands
- system
- brand
- OAuth
- SAML
- LDAP
- groups
- compliance
- cluster
- cloud
- elasticsearch
- bleve
- data retention
- jobs
- plugins
- roles
- schemes
- integration_actions
- remote clusters
- shared channels
- terms of service
- imports
- permissions
- exports
- usage
- reports
- custom profile attributes
- metrics
servers:
- url: "{your-mattermost-url}"
variables:
your-mattermost-url:
default: http://localhost:8065
- url: http://your-mattermost-url.com
- url: https://your-mattermost-url.com
paths:

Просмотреть файл

@@ -44,95 +44,6 @@
$ref: "#/components/responses/InternalServerError"
"501":
$ref: "#/components/responses/NotImplemented"
/api/v4/ldap/test_connection:
post:
tags:
- LDAP
summary: Test LDAP connection with specific settings
description: >
Test the LDAP connection using the provided settings without modifying
the current server configuration.
##### Permissions
Must have `sysconsole_read_authentication_ldap` or `manage_system` permission.
operationId: TestLdapConnection
requestBody:
description: LDAP settings to test
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LdapSettings"
responses:
"200":
description: LDAP connection test successful
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
"501":
$ref: "#/components/responses/NotImplemented"
/api/v4/ldap/test_diagnostics:
post:
tags:
- LDAP
summary: Test LDAP diagnostics with specific settings
description: >
Test LDAP diagnostics using the provided settings to validate configuration
and see sample results without modifying the current server configuration.
Use the `test` query parameter to specify which diagnostic to run.
##### Permissions
Must have `sysconsole_read_authentication_ldap` or `manage_system` permission.
operationId: TestLdapDiagnostics
parameters:
- in: query
name: test
required: true
description: Type of LDAP diagnostic test to run
schema:
type: string
enum:
- filters
- attributes
- group_attributes
example: filters
requestBody:
description: LDAP settings to test diagnostics with
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LdapSettings"
responses:
"200":
description: LDAP diagnostic test results
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/LdapDiagnosticResult"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
"501":
$ref: "#/components/responses/NotImplemented"
/api/v4/ldap/groups:
get:
tags:

Просмотреть файл

@@ -401,12 +401,6 @@
schema:
type: integer
default: 0
- name: fromUpdateAt
in: query
description: The update_at timestamp to return the next page of posts from. You cannot set this flag with direction=down.
schema:
type: integer
default: 0
- name: direction
in: query
description: The direction to return the posts. Either up or down.
@@ -431,12 +425,6 @@
schema:
type: boolean
default: false
- name: updatesOnly
in: query
description: This flag is used to make the API work with the updateAt value. If you set this flag, you must set a value for fromUpdateAt.
schema:
type: boolean
default: false
responses:
"200":
description: Post list retrieval successful

Просмотреть файл

@@ -100,8 +100,7 @@
##### Permissions
Must have `sysconsole_write_user_management_permissions` or `manage_system` permission.
When updating the role of a system admin, the `manage_system` permission is mandatory.
`manage_system` permission is required.
__Minimum server version__: 4.9

Просмотреть файл

@@ -226,88 +226,3 @@
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"/api/v4/sharedchannels/{channel_id}/remotes":
get:
tags:
- shared channels
summary: Get remote clusters for a shared channel
description: |
Gets the remote clusters information for a shared channel.
__Minimum server version__: 10.11
##### Permissions
Must be authenticated and have the `read_channel` permission for the channel.
operationId: GetSharedChannelRemotes
parameters:
- name: channel_id
in: path
description: Channel GUID
required: true
schema:
type: string
responses:
"200":
description: Remote clusters retrieval successful
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/RemoteClusterInfo"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"/api/v4/sharedchannels/users/{user_id}/can_dm/{other_user_id}":
get:
tags:
- shared channels
summary: Check if user can DM another user in shared channels context
description: |
Checks if a user can send direct messages to another user, considering shared channel restrictions.
This is specifically for shared channels where DMs require direct connections between clusters.
__Minimum server version__: 10.11
##### Permissions
Must be authenticated and have permission to view the user.
operationId: CanUserDirectMessage
parameters:
- name: user_id
in: path
description: User GUID
required: true
schema:
type: string
- name: other_user_id
in: path
description: Other user GUID
required: true
schema:
type: string
responses:
"200":
description: DM permission check successful
content:
application/json:
schema:
type: object
properties:
can_dm:
type: boolean
description: Whether the user can send DMs to the other user
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"

Просмотреть файл

@@ -611,37 +611,6 @@
$ref: "#/components/responses/BadRequest"
"501":
$ref: "#/components/responses/NotImplemented"
/api/v4/license/load_metric:
get:
tags:
- system
summary: Get license load metric
description: >
Get the current license load metric, calculated based on monthly active users
against the licensed user count. Returns a value of 0 when there is no license loaded or
the license doesn't have a user count.
__Minimum server version__: 10.8
##### Permissions
Must be logged in.
operationId: GetLicenseLoadMetric
responses:
"200":
description: License load metric retrieval successful
content:
application/json:
schema:
type: object
properties:
load:
type: integer
description: Current license load metric as an integer
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/license/renewal:
get:
tags:

Просмотреть файл

@@ -74,61 +74,6 @@
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/api/v4/users/login/sso/code-exchange:
post:
deprecated: true
tags:
- users
summary: Exchange SSO login code for session tokens
description: >
Exchange a short-lived login_code for session tokens using SAML code exchange (mobile SSO flow).
**Deprecated:** This endpoint is deprecated and will be removed in a future release.
Mobile clients should use the direct SSO callback flow instead.
##### Permissions
No permission required.
operationId: LoginSSOCodeExchange
requestBody:
content:
application/json:
schema:
type: object
required:
- login_code
- code_verifier
- state
properties:
login_code:
description: Short-lived one-time code from SSO callback
type: string
code_verifier:
description: SAML verifier to prove code possession
type: string
state:
description: State parameter to prevent CSRF attacks
type: string
description: SSO code exchange object
required: true
responses:
"200":
description: Code exchange successful
content:
application/json:
schema:
type: object
properties:
token:
description: Session token for authentication
type: string
csrf:
description: CSRF token for request validation
type: string
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
/api/v4/users/logout:
post:
tags:
@@ -3321,25 +3266,3 @@
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"/api/v4/users/{user_id}/reset_failed_attempts":
post:
tags:
- users
summary: Reset the failed password attempts for a user
description: |
Reset the FailedAttempts field for a user to 0. This will only work for ldap and email/password users.
##### Permissions
Requires `sysconsole_write_user_management_users` permission.
operationId: resetPasswordFailedAttempts
responses:
"200":
description: User's thread update successful
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"

Просмотреть файл

@@ -58,7 +58,7 @@ mme2e_wait_image () {
IMAGE_NAME=${1?}
RETRIES_LEFT=${2:-1}
RETRIES_INTERVAL=${3:-10}
mme2e_wait_command_success "docker pull --platform linux/amd64 $IMAGE_NAME" "Waiting for docker image ${IMAGE_NAME} to be available" "$RETRIES_LEFT" "$RETRIES_INTERVAL"
mme2e_wait_command_success "docker pull $IMAGE_NAME" "Waiting for docker image ${IMAGE_NAME} to be available" "$RETRIES_LEFT" "$RETRIES_INTERVAL"
}
mme2e_is_token_in_list() {
local TOKEN=$1
@@ -98,7 +98,7 @@ case "${TEST:-$TEST_DEFAULT}" in
cypress )
export TEST_FILTER_DEFAULT='--stage=@prod --group=@smoke' ;;
playwright )
export TEST_FILTER_DEFAULT='functional/system_console/system_users/actions.spec.ts' ;;
export TEST_FILTER_DEFAULT='tests/functional/system_console/system_users/actions.spec.ts --project=chrome' ;;
* )
export TEST_FILTER_DEFAULT='' ;;
esac

Просмотреть файл

@@ -14,20 +14,17 @@ cd "$(dirname "$0")"
: ${WEBHOOK_URL:-} # Optional. Mattermost webhook to post the report back to
: ${RELEASE_DATE:-} # Optional. If set, its value will be included in the report as the release date of the tested artifact
if [ "$TYPE" = "PR" ]; then
# Try to determine PR number: first from PR_NUMBER, then from BRANCH (server-pr-XXXX format)
if [ -n "${PR_NUMBER:-}" ]; then
export PULL_REQUEST="https://github.com/mattermost/mattermost/pull/${PR_NUMBER}"
elif grep -qE '^server-pr-[0-9]+$' <<<"${BRANCH:-}"; then
PR_NUMBER="${BRANCH##*-}"
export PULL_REQUEST="https://github.com/mattermost/mattermost/pull/${PR_NUMBER}"
else
mme2e_log "Warning: TYPE=PR but cannot determine PR number from PR_NUMBER or BRANCH. Falling back to TYPE=NONE."
TYPE=NONE
# In this case, we expect the PR number to be present in the BRANCH variable
BRANCH_REGEX='^server-pr-[0-9]+$'
if ! grep -qE "${BRANCH_REGEX}"<<<"$BRANCH"; then
mme2e_log "Error: when using TYPE=PR, the BRANCH variable should respect regex '$BRANCH_REGEX'. Aborting." >&2
exit 1
fi
export PULL_REQUEST="https://github.com/mattermost/mattermost/pull/${BRANCH##*-}"
fi
# Env vars used during the test. Their values will be included in the report
: ${TEST:?} # See E2E tests' readme
: ${TEST:?} # See E2E tests' readme
: ${BRANCH:?} # May be either a ref, a commit hash, or 'server-pr-PR_NUMBER' (if TYPE=PR)
: ${BUILD_ID:?}
: ${SERVER:?} # May be either 'onprem' or 'cloud'
@@ -59,27 +56,20 @@ if [ -n "${TM4J_API_KEY:-}" ]; then
# Assert that the test type is among the ones supported by Zephyr, and select the corresponding folderId
case "${SERVER}-${TYPE}" in
onprem-RELEASE)
export TM4J_FOLDER_ID="2014475"
;;
export TM4J_FOLDER_ID="2014475" ;;
onprem-MASTER)
export TM4J_FOLDER_ID="2014476"
;;
export TM4J_FOLDER_ID="2014476" ;;
onprem-MASTER_UNSTABLE)
export TM4J_FOLDER_ID="2014478"
;;
export TM4J_FOLDER_ID="2014478" ;;
cloud-RELEASE)
export TM4J_FOLDER_ID="2014474"
;;
export TM4J_FOLDER_ID="2014474" ;;
cloud-CLOUD)
export TM4J_FOLDER_ID="2014479"
;;
export TM4J_FOLDER_ID="2014479" ;;
cloud-CLOUD_UNSTABLE)
export TM4J_FOLDER_ID="2014481"
;;
export TM4J_FOLDER_ID="2014481" ;;
*)
mme2e_log "Error: unsupported Zephyr environment for the requested report (SERVER=${SERVER}, TYPE=${TYPE}). Aborting." >&2
exit 1
;;
esac
: ${TEST_CYCLE_LINK_PREFIX:?}
: ${TM4J_CYCLE_KEY:-} # Optional. Populated automatically by the reporting script
@@ -107,15 +97,15 @@ if [ ! -d "results/" ]; then
fi
case "$TEST" in
cypress)
npm i
node save_report.js
;;
playwright)
if [ -n "$WEBHOOK_URL" ]; then
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i
# Utilize environment data and report files to generate the webhook body
node report.webhookgen.js | curl -X POST -fsSL -H 'Content-Type: application/json' -d @- "$WEBHOOK_URL"
fi
;;
cypress)
npm i
node save_report.js
;;
playwright)
if [ -n "$WEBHOOK_URL" ]; then
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i
# Utilize environment data and report files to generate the webhook body
./report.webhookgen.js | curl -X POST -fsSL -H 'Content-Type: application/json' -d @- "$WEBHOOK_URL"
fi
;;
esac

Просмотреть файл

@@ -48,7 +48,6 @@ generate_docker_compose_file() {
services:
server:
image: \${SERVER_IMAGE}
platform: linux/amd64
restart: always
env_file:
- "./.env.server"
@@ -62,12 +61,9 @@ services:
MM_CLUSTERSETTINGS_READONLYCONFIG: "false"
MM_SERVICEENVIRONMENT: "test"
MM_FEATUREFLAGS_MOVETHREADSENABLED: "true"
MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES: "true"
MM_LOGSETTINGS_ENABLEDIAGNOSTICS: "false"
MM_LOGSETTINGS_CONSOLELEVEL: "DEBUG"
network_mode: host
ports:
- "8065:8065"
depends_on:
$(for service in $ENABLED_DOCKER_SERVICES; do
# The server container will start only if all other dependent services are healthy
@@ -82,7 +78,7 @@ $(for service in $ENABLED_DOCKER_SERVICES; do
$(if mme2e_is_token_in_list "postgres" "$ENABLED_DOCKER_SERVICES"; then
echo '
postgres:
image: mattermostdevelopment/mirrored-postgres:14
image: mattermostdevelopment/mirrored-postgres:13
restart: "no"
network_mode: host
networks: !reset []
@@ -140,7 +136,7 @@ $(if mme2e_is_token_in_list "openldap" "$ENABLED_DOCKER_SERVICES"; then
fi)
$(if mme2e_is_token_in_list "elasticsearch" "$ENABLED_DOCKER_SERVICES"; then
echo '
echo '
elasticsearch:
restart: "no"
network_mode: host
@@ -164,7 +160,7 @@ $(if mme2e_is_token_in_list "elasticsearch" "$ENABLED_DOCKER_SERVICES"; then
fi)
$(if mme2e_is_token_in_list "opensearch" "$ENABLED_DOCKER_SERVICES"; then
echo '
echo '
opensearch:
restart: "no"
network_mode: host
@@ -182,7 +178,7 @@ $(if mme2e_is_token_in_list "opensearch" "$ENABLED_DOCKER_SERVICES"; then
fi)
$(if mme2e_is_token_in_list "redis" "$ENABLED_DOCKER_SERVICES"; then
echo '
echo '
redis:
restart: "no"
network_mode: host
@@ -280,7 +276,7 @@ $(if mme2e_is_token_in_list "webhook-interactions" "$ENABLED_DOCKER_SERVICES"; t
$(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
echo '
playwright:
image: mcr.microsoft.com/playwright:v1.53.0-noble
image: mcr.microsoft.com/playwright:v1.49.1-noble
entrypoint: ["/bin/bash", "-c"]
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"]
env_file:
@@ -297,7 +293,7 @@ $(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
PW_RESET_BEFORE_TEST: "false"
PW_HEADLESS: "true"
PW_SLOWMO: 0
PW_WORKERS: 1
PW_WORKERS: 2
PW_SNAPSHOT_ENABLE: "false"
PW_PERCY_ENABLE: "false"
ulimits:
@@ -339,11 +335,11 @@ generate_env_files() {
# Generating service-specific env vars
for SERVICE in $ENABLED_DOCKER_SERVICES; do
case $SERVICE in
case $SERVICE in
opensearch)
echo "MM_ELASTICSEARCHSETTINGS_BACKEND=opensearch" >>.env.server
;;
esac
esac
done
# Generating TEST-specific env files
@@ -377,7 +373,7 @@ generate_env_files() {
keycloak)
echo "CYPRESS_keycloakBaseUrl=http://localhost:8484" >>.env.cypress
;;
elasticsearch | opensearch)
elasticsearch|opensearch)
echo "CYPRESS_elasticsearchConnectionURL=http://localhost:9200" >>.env.cypress
;;
esac
@@ -386,8 +382,8 @@ generate_env_files() {
case "$SERVER" in
cloud)
echo "CYPRESS_serverEdition=Cloud" >>.env.cypress
echo "CYPRESS_cwsURL=${CWS_URL}" >>.env.cypress
echo "CYPRESS_cwsAPIURL=${CWS_URL}" >>.env.cypress
echo "CYPRESS_cwsURL=${CWS_URL}" >> .env.cypress
echo "CYPRESS_cwsAPIURL=${CWS_URL}" >> .env.cypress
;;
*)
echo "CYPRESS_serverEdition=E20" >>.env.cypress

Просмотреть файл

@@ -5,10 +5,11 @@ cd "$(dirname "$0")"
mme2e_log "Configuring starting server parameters that may be changed at runtime"
for SETTING in \
TeamSettings.EnableOpenServer=true \
PluginSettings.Enable=true \
PluginSettings.EnableUploads=true \
PluginSettings.AutomaticPrepackagedPlugins=true; do
TeamSettings.EnableOpenServer=true \
PluginSettings.Enable=true \
PluginSettings.EnableUploads=true \
PluginSettings.AutomaticPrepackagedPlugins=true
do
mme2e_log "Configuring parameter: $SETTING"
# shellcheck disable=SC2046
${MME2E_DC_SERVER} exec -T -- server mmctl --local config set $(tr '=' ' ' <<<$SETTING)
@@ -22,30 +23,24 @@ fi
if [ "$TEST" = "cypress" ]; then
mme2e_log "Prepare Cypress: install dependencies"
# Create user in Cypress container and work around the node image's assumption that the app files are owned by user 1000
mme2e_log "Creating user for Cypress container"
${MME2E_DC_SERVER} exec -T -u 0 -- cypress bash -c "id $MME2E_UID || useradd -u $MME2E_UID -m nodeci"
${MME2E_DC_SERVER} exec -T -u 0 -- cypress bash -c "id $MME2E_UID || useradd -u $MME2E_UID -m nodeci" # Works around the node image's assumption that the app files are owned by user 1000
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress npm i
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress cypress install
mme2e_log "Prepare Cypress: populating fixtures"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress tee tests/fixtures/keycloak.crt >/dev/null <../../server/build/docker/keycloak/keycloak.crt
# Download plugins using wget in the Cypress container
for PLUGIN_URL in \
"https://github.com/mattermost/mattermost-plugin-gitlab/releases/download/v1.3.0/com.github.manland.mattermost-plugin-gitlab-1.3.0.tar.gz" \
"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.9.0/com.mattermost.demo-plugin-0.9.0.tar.gz" \
"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.8.0/com.mattermost.demo-plugin-0.8.0.tar.gz"; do
"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.8.0/com.mattermost.demo-plugin-0.8.0.tar.gz"
do
PLUGIN_NAME="${PLUGIN_URL##*/}"
PLUGIN_PATH="tests/fixtures/$PLUGIN_NAME"
# Check if file exists
if ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress test -f "$PLUGIN_PATH"; then
mme2e_log "Skipping installation of plugin $PLUGIN_NAME: file exists"
continue
fi
# Use wget in the Cypress container instead of curl in server (for distroless compatibility)
mme2e_log "Downloading $PLUGIN_NAME to fixtures directory"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress wget -q -O "/cypress/$PLUGIN_PATH" "$PLUGIN_URL"
${MME2E_DC_SERVER} exec -T -- server curl -L --silent "${PLUGIN_URL}" | ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress tee "$PLUGIN_PATH" >/dev/null
done
fi
@@ -60,8 +55,6 @@ for SERVICE in $ENABLED_DOCKER_SERVICES; do
continue
fi
mme2e_log "Configuring the $SERVICE container"
${MME2E_DC_SERVER} exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest || true' <../../server/tests/custom-schema-objectID.ldif
${MME2E_DC_SERVER} exec -T -- openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest || true' <../../server/tests/custom-schema-cpa.ldif
${MME2E_DC_SERVER} exec -T -- openldap bash -c 'ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest' <../../server/tests/test-data.ldif
;;
minio)

Просмотреть файл

@@ -42,7 +42,7 @@ else
fi
# Collect run results
cat >../cypress/results/summary.json <<EOF
cat > ../cypress/results/summary.json <<EOF
{
"passed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.passes' | jq -s add),
"failed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.failures' | jq -s add),
@@ -51,4 +51,4 @@ cat >../cypress/results/summary.json <<EOF
EOF
# Collect server logs
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"../cypress/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1
${MME2E_DC_SERVER} logs --no-log-prefix -- server > "../cypress/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1

Просмотреть файл

@@ -37,12 +37,14 @@ EOF
# Run Playwright test
# NB: do not exit the script if some testcases fail
${MME2E_DC_SERVER} exec -i -u "$MME2E_UID" -- playwright bash -c "cd e2e-tests/playwright && npm run test:ci -- ${TEST_FILTER} ${PW_SHARD:-}" | tee ../playwright/logs/playwright.log || true
${MME2E_DC_SERVER} exec -i -u "$MME2E_UID" -- playwright bash -c "cd e2e-tests/playwright && npm run test -- ${TEST_FILTER}" | tee ../playwright/logs/playwright.log || true
# Collect run results
# Documentation on the results.json file: https://playwright.dev/docs/api/class-testcase#test-case-expected-status
jq -f /dev/stdin ../playwright/results/reporter/results.json >../playwright/results/summary.json <<EOF
# NB: the following line is needed only for compatibility reasons, to support RollingRelease tests for versions prior to v10.1.0
# It can be removed after releases <=v10.0.x are phased out
mv -v ../playwright/playwright-report/results.json ../playwright/results/reporter/results.json 2>/dev/null || true
jq -f /dev/stdin ../playwright/results/reporter/results.json > ../playwright/results/summary.json <<EOF
{
passed: .stats.expected,
failed: .stats.unexpected,

Просмотреть файл

@@ -1,101 +0,0 @@
#!/bin/bash
# shellcheck disable=SC2038
# Run specific spec files
# Usage: SPEC_FILES="path/to/spec1.ts,path/to/spec2.ts" make start-server run-specs
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
if [ -z "${SPEC_FILES:-}" ]; then
mme2e_log "Error: SPEC_FILES environment variable is required"
mme2e_log "Usage: SPEC_FILES=\"path/to/spec.ts\" make start-server run-specs"
exit 1
fi
mme2e_log "Running spec files: $SPEC_FILES"
case $TEST in
cypress)
mme2e_log "Running Cypress with specified specs"
# Initialize cypress report directory
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<EOF
rm -rf logs results
mkdir -p logs
mkdir -p results/junit
mkdir -p results/mochawesome-report/json/tests
touch results/junit/empty.xml
echo '<?xml version="1.0" encoding="UTF-8"?>' > results/junit/empty.xml
EOF
# Run cypress with specific spec files and mochawesome reporter
LOGFILE_SUFFIX="${CI_BASE_URL//\//_}_specs"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress npx cypress run \
--spec "$SPEC_FILES" \
--reporter cypress-multi-reporters \
--reporter-options configFile=reporter-config.json \
| tee "../cypress/logs/${LOGFILE_SUFFIX}_cypress.log" || true
# Collect run results
if [ -d ../cypress/results/mochawesome-report/json/tests/ ]; then
cat >../cypress/results/summary.json <<EOF
{
"passed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.passes' | jq -s add),
"failed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.failures' | jq -s add),
"failed_expected": 0
}
EOF
fi
# Collect server logs
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"../cypress/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1
;;
playwright)
mme2e_log "Running Playwright with specified specs"
# Convert comma-separated to space-separated for playwright
SPEC_ARGS=$(echo "$SPEC_FILES" | tr ',' ' ')
# Initialize playwright report and logs directory
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
cd e2e-tests/playwright
rm -rf logs results storage_state
mkdir -p logs results
touch logs/mattermost.log
EOF
# Install dependencies
mme2e_log "Prepare Playwright: install dependencies"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
cd webapp/
npm install --cache /tmp/empty-cache
cd ../e2e-tests/playwright
npm install --cache /tmp/empty-cache
EOF
# Run playwright with specific spec files
LOGFILE_SUFFIX="${CI_BASE_URL//\//_}_specs"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -c "cd e2e-tests/playwright && npm run test:ci -- $SPEC_ARGS" | tee "../playwright/logs/${LOGFILE_SUFFIX}_playwright.log" || true
# Collect run results (if results.json exists)
if [ -f ../playwright/results/reporter/results.json ]; then
jq -f /dev/stdin ../playwright/results/reporter/results.json >../playwright/results/summary.json <<EOF
{
passed: .stats.expected,
failed: .stats.unexpected,
failed_expected: (.stats.skipped + .stats.flaky)
}
EOF
mme2e_log "Results file found and summary generated"
fi
# Collect server logs
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"../playwright/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1
;;
*)
mme2e_log "Error, unsupported value for TEST: $TEST" >&2
mme2e_log "Aborting" >&2
exit 1
;;
esac
mme2e_log "Spec run complete"

Просмотреть файл

@@ -11,13 +11,6 @@ mme2e_wait_image "$SERVER_IMAGE" 4 30
mme2e_log "Starting E2E containers"
${MME2E_DC_SERVER} create
${MME2E_DC_SERVER} up -d --remove-orphans
# Postgres check
if ! mme2e_wait_command_success "${MME2E_DC_SERVER} exec -T -- postgres pg_isready -h localhost" "Waiting for postgres to accept connections" "30" "5"; then
mme2e_log "Postgres not accepting connections"
exit 1
fi
if ! mme2e_wait_service_healthy server 60 10; then
mme2e_log "Mattermost container not healthy, retry attempts exhausted. Giving up." >&2
exit 1
@@ -25,21 +18,9 @@ fi
# shellcheck disable=SC2043
for MIGRATION in migration_advanced_permissions_phase_2; do
# Query explanation: if it doesn't find the migration in the table, there are 0 results and the command fails with a divide-by-zero error. Otherwise the command succeeds
MIGRATION_CHECK_COMMAND="${MME2E_DC_SERVER} exec -T postgres sh -c 'PGPASSWORD=mostest psql -U mmuser mattermost_test -c \"select 1 / (select count(*) from Systems where name = '\''${MIGRATION}'\'' and value = '\''true'\'');\"'"
if ! mme2e_wait_command_success "$MIGRATION_CHECK_COMMAND" "Waiting for migration to be completed: ${MIGRATION}" "10" "10"; then
MIGRATION_CHECK_COMMAND="${MME2E_DC_SERVER} exec -T -- postgres psql -U mmuser mattermost_test -c \"select 1 / (select count(*) from Systems where name = '${MIGRATION}' and value = 'true');\""
if ! mme2e_wait_command_success "$MIGRATION_CHECK_COMMAND >/dev/null 2>&1" "Waiting for migration to be completed: ${MIGRATION}" "30" "10"; then
mme2e_log "Migration ${MIGRATION} not completed, retry attempts exhausted. Giving up." >&2
LOG_DIR="../${TEST}/logs"
mkdir -p "$LOG_DIR"
# Save server logs to a file
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"$LOG_DIR/server.log"
mme2e_log "Server logs saved to server.log"
# Save postgres logs to a file
${MME2E_DC_SERVER} logs --no-log-prefix -- postgres >"$LOG_DIR/postgres.log"
mme2e_log "Postgres logs saved to postgres.log"
exit 2
fi
mme2e_log "${MIGRATION}: completed."

Просмотреть файл

@@ -9,7 +9,7 @@ clean:
rm -fv .ci/server.yml
rm -fv .ci/.env.{server,dashboard,cypress,playwright}
.PHONY: generate-server start-server run-test run-specs stop-server restart-server
.PHONY: generate-server start-server run-test stop-server restart-server
generate-server:
bash ./.ci/server.generate.sh
start-server: generate-server
@@ -17,8 +17,6 @@ start-server: generate-server
bash ./.ci/server.prepare.sh
run-test:
bash ./.ci/server.run_test.sh
run-specs:
bash ./.ci/server.run_specs.sh
stop-server: generate-server
bash ./.ci/server.stop.sh
restart-server: stop-server start-server

17
e2e-tests/cypress/package-lock.json сгенерированный
Просмотреть файл

@@ -42,7 +42,6 @@
"cypress-file-upload": "5.0.8",
"cypress-multi-reporters": "1.6.4",
"cypress-plugin-tab": "1.0.5",
"cypress-real-events": "1.14.0",
"cypress-wait-until": "3.0.1",
"dayjs": "1.11.10",
"deepmerge": "4.3.1",
@@ -4612,15 +4611,6 @@
"ally.js": "^1.4.1"
}
},
"node_modules/cypress-real-events": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/cypress-real-events/-/cypress-real-events-1.14.0.tgz",
"integrity": "sha512-XmI8y3OZLh6cjRroPalzzS++iv+pGCaD9G9kfIbtspgv7GVsDt30dkZvSXfgZb4rAN+3pOkMVB7e0j4oXydW7Q==",
"dev": true,
"peerDependencies": {
"cypress": "^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x || ^14.x"
}
},
"node_modules/cypress-wait-until": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-3.0.1.tgz",
@@ -14941,13 +14931,6 @@
"ally.js": "^1.4.1"
}
},
"cypress-real-events": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/cypress-real-events/-/cypress-real-events-1.14.0.tgz",
"integrity": "sha512-XmI8y3OZLh6cjRroPalzzS++iv+pGCaD9G9kfIbtspgv7GVsDt30dkZvSXfgZb4rAN+3pOkMVB7e0j4oXydW7Q==",
"dev": true,
"requires": {}
},
"cypress-wait-until": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-3.0.1.tgz",

Просмотреть файл

@@ -37,7 +37,6 @@
"cypress-file-upload": "5.0.8",
"cypress-multi-reporters": "1.6.4",
"cypress-plugin-tab": "1.0.5",
"cypress-real-events": "1.14.0",
"cypress-wait-until": "3.0.1",
"dayjs": "1.11.10",
"deepmerge": "4.3.1",

Двоичные данные
e2e-tests/cypress/tests/fixtures/bot-default-avatar.png поставляемый

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.4 KiB

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

23
e2e-tests/cypress/tests/fixtures/playbook-export.json поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,23 @@
{
"checklists": [
{
"items": [
{
"title": "Untitled task"
}
],
"title": "Default checklist"
}
],
"create_channel_member_on_new_participant": true,
"create_channel_member_on_removed_participant": true,
"description": "Customize this playbook's description to give an overview of when and how this playbook is run.",
"message_on_join": "Welcome! This channel was automatically created as part of a playbook run.",
"metrics": [],
"reminder_timer_default_seconds": 604800,
"retrospective_enabled": true,
"retrospective_template": "### Summary\nThis should contain 2-3 sentences that give a reader an overview of what happened, what was the cause, and what was done. The briefer the better as this is what future teams will look at first for reference.\n\n### What was the impact?\nThis section describes the impact of this playbook run as experienced by internal and external customers as well as stakeholders.\n\n### What were the contributing factors?\nThis playbook may be a reactive protocol to a situation that is otherwise undesirable. If that's the case, this section explains the reasons that caused the situation in the first place. There may be multiple root causes - this helps stakeholders understand why.\n\n### What was done?\nThis section tells the story of how the team collaborated throughout the event to achieve the outcome. This will help future teams learn from this experience on what they could try.\n\n### What did we learn?\nThis section should include perspective from everyone that was involved to celebrate the victories and identify areas for improvement. For example: What went well? What didn't go well? What should be done differently next time?\n\n### Follow-up tasks\nThis section lists the action items to turn learnings into changes that help the team become more proficient with iterations. It could include tweaking the playbook, publishing the retrospective, or other improvements. The best follow-ups will have a clear owner as well as due date.\n\n### Timeline highlights\nThis section is a curated log that details the most important moments. It can contain key communications, screen shots, or other artifacts. Use the built-in timeline feature to help you retrace and replay the sequence of events.",
"status_update_enabled": true,
"title": "Example Playbook",
"version": 1
}

Просмотреть файл

@@ -50,7 +50,6 @@ describe('Verify Accessibility Support in different sections in Settings and Pro
{key: 'click_to_reply', label: 'Click to open threads', type: 'radio'},
{key: 'channel_display_mode', label: 'Channel Display', type: 'radio'},
{key: 'one_click_reactions_enabled', label: 'Quick reactions on messages', type: 'radio'},
{key: 'renderEmoticonsAsEmoji', label: 'Render emoticons as emojis', type: 'radio'},
{key: 'languages', label: 'Language', type: 'dropdown'},
],
sidebar: [
@@ -170,43 +169,36 @@ describe('Verify Accessibility Support in different sections in Settings and Pro
cy.get('#displayButton').click();
cy.get('#languagesEdit').click();
cy.findByRole('combobox', {name: 'Dropdown selector to change the interface language'}).should('have.attr', 'aria-autocomplete', 'list').and('have.attr', 'aria-labelledby', 'changeInterfaceLanguageLabel').as('inputEl');
cy.get('#displayLanguage').within(() => {
cy.get('input').should('have.attr', 'aria-autocomplete', 'list').and('have.attr', 'aria-labelledby', 'changeInterfaceLanguageLabel').as('inputEl');
});
cy.get('#changeInterfaceLanguageLabel').should('be.visible').and('have.text', 'Change interface language');
// # When space key is pressed on dropdown, it should expand and should collapse when esc key is pressed
cy.get('@inputEl').typeWithForce(' ');
cy.findByRole('listbox').should('have.class', 'react-select__menu-list').as('listBox');
cy.get('@inputEl').typeWithForce('{esc}');
cy.get('@listBox').should('not.exist');
// # When enter key is pressed on dropdown, it should expand and collapse
cy.get('@inputEl').typeWithForce('{enter}');
cy.get('#displayLanguage>div').should('have.class', 'react-select__control--menu-is-open');
cy.get('@inputEl').typeWithForce('{enter}');
cy.get('#displayLanguage>div').should('not.have.class', 'react-select__control--menu-is-open');
// # Press down arrow twice and check aria label
cy.get('@inputEl').typeWithForce(' ');
cy.get('@inputEl').typeWithForce('{enter}');
cy.get('@inputEl').typeWithForce('{downarrow}{downarrow}');
cy.get('#displayLanguage').within(($el) => {
cy.wrap($el).findByRole('log').should('have.attr', 'aria-live', 'assertive').as('ariaEl');
});
cy.get('@ariaEl').within(($el) => {
cy.wrap($el).get('#aria-focused').should('contain', 'option English (Australia) focused');
cy.wrap($el).get('#aria-guidance').should('contain', 'Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.');
cy.get('#displayLanguage>span').as('ariaEl').within(($el) => {
cy.wrap($el).should('have.attr', 'aria-live', 'assertive');
cy.get('#aria-context').should('contain', 'option English (Australia) focused').and('contain', 'Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.');
});
// # Check if language setting gets changed after user presses space
cy.get('@inputEl').typeWithForce(' ');
// # Check if language setting gets changed after user presses enter
cy.get('@inputEl').typeWithForce('{enter}');
cy.get('#displayLanguage').should('contain', 'English (Australia)');
cy.get('@ariaEl').within(($el) => {
cy.wrap($el).get('#aria-selection').should('contain', 'option English (Australia) selected');
});
cy.get('@ariaEl').get('#aria-selection-event').should('contain', 'option English (Australia), selected');
// # Press down arrow, then up arrow and press space
// # Press down arrow, then up arrow and press enter
cy.get('@inputEl').typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}');
cy.get('@ariaEl').within(($el) => {
cy.wrap($el).get('#aria-focused').should('contain', 'option English (US) focused');
});
cy.get('@inputEl').typeWithForce(' ');
cy.get('@ariaEl').get('#aria-context').should('contain', 'option English (US) focused');
cy.get('@inputEl').typeWithForce('{enter}');
cy.get('#displayLanguage').should('contain', 'English (US)');
cy.get('@ariaEl').within(($el) => {
cy.wrap($el).get('#aria-selection').should('contain', 'option English (US) selected');
});
cy.get('@ariaEl').get('#aria-selection-event').should('contain', 'option English (US), selected');
});
it('MM-T1488 Profile Picture should read labels', () => {

Просмотреть файл

@@ -105,9 +105,9 @@ describe('Verify Accessibility Support in Dropdown Menus', () => {
{id: 'invitePeople', label: 'Invite People dialog'},
{id: 'teamSettings', label: 'Team Settings dialog'},
{id: 'manageMembers', label: 'Manage Members dialog'},
{id: 'joinTeam', text: 'Join another team'},
{id: 'joinTeam', text: 'Join Another Team'},
{id: 'leaveTeam', label: 'Leave Team dialog'},
{id: 'createTeam', text: 'Create a team'},
{id: 'createTeam', text: 'Create a Team'},
];
menuItems.forEach((item) => {

Просмотреть файл

@@ -7,6 +7,7 @@
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @accessibility
import {getRandomId} from '../../../utils';
@@ -69,12 +70,6 @@ describe('Verify Accessibility keyboard usability across different regions in th
cy.get('body').type('{leftarrow}').wait(100);
cy.get('#messagesTab').should('have.class', 'a11y--active a11y--focused');
// # move focus to the team selector
cy.focused().tab();
// * Ensures the focus item has test-id of searchTeamsSelectorMenuButton
cy.get('#searchTeamsSelectorMenuButton').should('have.class', 'a11y--active a11y--focused');
// # move focus to search results items
cy.focused().tab();

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше