Сравнить коммиты
3 Коммитов
release-10
...
master
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
eeba8e627d | ||
|
|
630bd908ed | ||
|
|
f43717aaa0 |
64
.github/ISSUE_TEMPLATE/bug_report.yml
поставляемый
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.
|
||||
2
.github/actions/calculate-cypress-results/.gitignore
поставляемый
2
.github/actions/calculate-cypress-results/.gitignore
поставляемый
@@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
.env
|
||||
50
.github/actions/calculate-cypress-results/action.yaml
поставляемый
50
.github/actions/calculate-cypress-results/action.yaml
поставляемый
@@ -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
|
||||
19347
.github/actions/calculate-cypress-results/dist/index.js
поставляемый
19347
.github/actions/calculate-cypress-results/dist/index.js
поставляемый
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
15
.github/actions/calculate-cypress-results/jest.config.js
поставляемый
15
.github/actions/calculate-cypress-results/jest.config.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
сгенерированный
поставляемый
9136
.github/actions/calculate-cypress-results/package-lock.json
сгенерированный
поставляемый
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
27
.github/actions/calculate-cypress-results/package.json
поставляемый
27
.github/actions/calculate-cypress-results/package.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"
|
||||
}
|
||||
}
|
||||
3
.github/actions/calculate-cypress-results/src/index.ts
поставляемый
3
.github/actions/calculate-cypress-results/src/index.ts
поставляемый
@@ -1,3 +0,0 @@
|
||||
import { run } from "./main";
|
||||
|
||||
run();
|
||||
101
.github/actions/calculate-cypress-results/src/main.ts
поставляемый
101
.github/actions/calculate-cypress-results/src/main.ts
поставляемый
@@ -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);
|
||||
}
|
||||
271
.github/actions/calculate-cypress-results/src/merge.test.ts
поставляемый
271
.github/actions/calculate-cypress-results/src/merge.test.ts
поставляемый
@@ -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);
|
||||
});
|
||||
});
|
||||
358
.github/actions/calculate-cypress-results/src/merge.ts
поставляемый
358
.github/actions/calculate-cypress-results/src/merge.ts
поставляемый
@@ -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 };
|
||||
}
|
||||
139
.github/actions/calculate-cypress-results/src/types.ts
поставляемый
139
.github/actions/calculate-cypress-results/src/types.ts
поставляемый
@@ -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;
|
||||
}
|
||||
17
.github/actions/calculate-cypress-results/tsconfig.json
поставляемый
17
.github/actions/calculate-cypress-results/tsconfig.json
поставляемый
@@ -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"}
|
||||
13
.github/actions/calculate-cypress-results/tsup.config.ts
поставляемый
13
.github/actions/calculate-cypress-results/tsup.config.ts
поставляемый
@@ -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: [/.*/],
|
||||
});
|
||||
2
.github/actions/calculate-playwright-results/.gitignore
поставляемый
2
.github/actions/calculate-playwright-results/.gitignore
поставляемый
@@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
.env
|
||||
53
.github/actions/calculate-playwright-results/action.yaml
поставляемый
53
.github/actions/calculate-playwright-results/action.yaml
поставляемый
@@ -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
|
||||
19323
.github/actions/calculate-playwright-results/dist/index.js
поставляемый
19323
.github/actions/calculate-playwright-results/dist/index.js
поставляемый
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
6
.github/actions/calculate-playwright-results/jest.config.js
поставляемый
6
.github/actions/calculate-playwright-results/jest.config.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
сгенерированный
поставляемый
9136
.github/actions/calculate-playwright-results/package-lock.json
сгенерированный
поставляемый
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
27
.github/actions/calculate-playwright-results/package.json
поставляемый
27
.github/actions/calculate-playwright-results/package.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"
|
||||
}
|
||||
}
|
||||
3
.github/actions/calculate-playwright-results/src/index.ts
поставляемый
3
.github/actions/calculate-playwright-results/src/index.ts
поставляемый
@@ -1,3 +0,0 @@
|
||||
import { run } from "./main";
|
||||
|
||||
run();
|
||||
123
.github/actions/calculate-playwright-results/src/main.ts
поставляемый
123
.github/actions/calculate-playwright-results/src/main.ts
поставляемый
@@ -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);
|
||||
}
|
||||
509
.github/actions/calculate-playwright-results/src/merge.test.ts
поставляемый
509
.github/actions/calculate-playwright-results/src/merge.test.ts
поставляемый
@@ -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);
|
||||
});
|
||||
});
|
||||
304
.github/actions/calculate-playwright-results/src/merge.ts
поставляемый
304
.github/actions/calculate-playwright-results/src/merge.ts
поставляемый
@@ -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,
|
||||
};
|
||||
}
|
||||
89
.github/actions/calculate-playwright-results/src/types.ts
поставляемый
89
.github/actions/calculate-playwright-results/src/types.ts
поставляемый
@@ -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;
|
||||
}
|
||||
17
.github/actions/calculate-playwright-results/tsconfig.json
поставляемый
17
.github/actions/calculate-playwright-results/tsconfig.json
поставляемый
@@ -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"}
|
||||
12
.github/actions/calculate-playwright-results/tsup.config.ts
поставляемый
12
.github/actions/calculate-playwright-results/tsup.config.ts
поставляемый
@@ -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
поставляемый
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
поставляемый
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
поставляемый
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
|
||||
|
||||
4
.github/workflows/build-opensearch-image.yml
поставляемый
4
.github/workflows/build-opensearch-image.yml
поставляемый
@@ -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
поставляемый
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
поставляемый
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
поставляемый
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
|
||||
|
||||
56
.github/workflows/dispatch-server-builder-image.yml
поставляемый
56
.github/workflows/dispatch-server-builder-image.yml
поставляемый
@@ -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
поставляемый
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
поставляемый
Обычный файл
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
поставляемый
Обычный файл
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
поставляемый
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"
|
||||
|
||||
649
.github/workflows/e2e-tests-cypress-template.yml
поставляемый
649
.github/workflows/e2e-tests-cypress-template.yml
поставляемый
@@ -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
поставляемый
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 }}
|
||||
583
.github/workflows/e2e-tests-playwright-template.yml
поставляемый
583
.github/workflows/e2e-tests-playwright-template.yml
поставляемый
@@ -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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
Обычный файл
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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
поставляемый
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/).
|
||||
|
||||
299
NOTICE.txt
299
NOTICE.txt
@@ -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.
|
||||
|
||||
|
||||
|
||||
11
README.md
11
README.md
@@ -1,6 +1,6 @@
|
||||
 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 can’t keep the SSO working, for exemple, as asked in [issue #4](https://framagit.org/framasoft/framateam/mostlymatter/-/issues/4) and can’t 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 you’re 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 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.
|
||||
|
||||
|
||||
```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
сгенерированный
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
поставляемый
Двоичные данные
e2e-tests/cypress/tests/fixtures/bot-default-avatar.png
поставляемый
Двоичный файл не отображается.
|
До Ширина: | Высота: | Размер: 2.4 KiB После Ширина: | Высота: | Размер: 3.3 KiB |
23
e2e-tests/cypress/tests/fixtures/playbook-export.json
поставляемый
Обычный файл
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();
|
||||
|
||||
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user