mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 18:38:54 +00:00
* chore(e2e): Fix e2e artifact upload path for CMT workflow * test(e2e): Replace MM-T placeholder test keys with real Zephyr Scale IDs - Classification Banner (18 tests): MM-T6196–MM-T6205, MM-T6206–MM-T6208, MM-T6209–MM-T6213 - mm_blocks (incoming webhook + ephemeral): MM-T6214–MM-T6240 (27 tests) - Fixes TM4J test-case linking by ensuring all test titles use real MM-T#### keys - Tightens getTM4JTestCases filter regex from /(MM-T)\w+/ to /MM-T\d+/ to prevent crashes when non-numeric MM-T placeholders slip through before Zephyr linkage - Scan all Xcode_*.app directories and filter for major version >= 26 to future-proof against Xcode 30+ releases (current glob pattern maxes at 29) - Extract version from directory name, validate major >= 26, sort and select highest - Add least-privilege job-level permissions (contents: read) to build jobs (build-ios-simulator, build-android-apk, build-android-apk-maestro) to scope actions/checkout and actions/upload-artifact access * fix: apply CodeRabbit review feedback Require Xcode 26.1+ for iOS prepare, standardize E2E titles to MM-TXXXX_N, and bound Zephyr test-case id matching to complete identifiers. * fix(e2e): Grant actions:write on CMT build jobs for artifact upload Job-level contents:read alone strips upload-artifact permission; match e2e-detox-pr.yml so ios/android/maestro builds can publish artifacts.
204 lines
6.4 KiB
JavaScript
204 lines
6.4 KiB
JavaScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
/* eslint-disable no-console, no-process-env */
|
|
|
|
// See reference: https://support.smartbear.com/tm4j-cloud/api-docs/
|
|
|
|
const axios = require('axios');
|
|
const chalk = require('chalk');
|
|
|
|
const status = {
|
|
passed: 'Pass',
|
|
failed: 'Fail',
|
|
pending: 'Pending',
|
|
skipped: 'Skip',
|
|
};
|
|
|
|
function getStepStateResult(steps = []) {
|
|
return steps.reduce((acc, item) => {
|
|
if (acc[item.state]) {
|
|
acc[item.state] += 1;
|
|
} else {
|
|
acc[item.state] = 1;
|
|
}
|
|
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
function getStepStateSummary(steps = []) {
|
|
const result = getStepStateResult(steps);
|
|
|
|
return Object.entries(result).map(([key, value]) => `${value} ${key}`).join(',');
|
|
}
|
|
|
|
// Bounded MM-T id: word boundary, digits, optional _N sub-test suffix.
|
|
const MM_T_ID_PATTERN = /(?:^|\s)MM-T\d+(?:_\d+)?(?=\s|$)/;
|
|
const MM_T_ID_EXTRACT = /(?:^|\s)(MM-T\d+)(?:_\d+)?(?=\s|$)/;
|
|
|
|
function getTM4JTestCases(allTests) {
|
|
return allTests.tests.
|
|
filter((item) => MM_T_ID_PATTERN.test(item.name)).
|
|
map((item) => {
|
|
return {
|
|
title: item.name,
|
|
duration: item.time,
|
|
incrementalDuration: item.incrementalDuration,
|
|
state: item.state,
|
|
pass: item.pass,
|
|
fail: item.fail,
|
|
pending: item.pending,
|
|
};
|
|
}).
|
|
reduce((acc, item) => {
|
|
// Zephyr case key is MM-T[digits]; optional _N is a sub-test index only.
|
|
const key = item.title.match(MM_T_ID_EXTRACT)[1];
|
|
|
|
if (acc[key]) {
|
|
acc[key].push(item);
|
|
} else {
|
|
acc[key] = [item];
|
|
}
|
|
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
function saveToEndpoint(url, data) {
|
|
return axios({
|
|
method: 'POST',
|
|
url,
|
|
headers: {
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
Authorization: process.env.ZEPHYR_API_KEY,
|
|
},
|
|
data,
|
|
}).catch((error) => {
|
|
console.log('Something went wrong:', error.response.data.message);
|
|
return error.response.data;
|
|
});
|
|
}
|
|
|
|
async function createTestCycle(startDate, endDate, options = {}) {
|
|
const {
|
|
BRANCH,
|
|
BUILD_ID,
|
|
COMMIT_HASH,
|
|
JIRA_PROJECT_KEY,
|
|
ZEPHYR_CYCLE_NAME,
|
|
ZEPHYR_FOLDER_ID,
|
|
} = process.env;
|
|
const framework = options.framework || 'Detox';
|
|
|
|
const testCycle = {
|
|
projectKey: JIRA_PROJECT_KEY,
|
|
name: ZEPHYR_CYCLE_NAME ? `${ZEPHYR_CYCLE_NAME} (${BUILD_ID}-${COMMIT_HASH}-${BRANCH})` : `${BUILD_ID}-${COMMIT_HASH}-${BRANCH}`,
|
|
description: `${framework} automated test with ${BRANCH}`,
|
|
plannedStartDate: startDate,
|
|
plannedEndDate: endDate,
|
|
statusName: 'Done',
|
|
folderId: ZEPHYR_FOLDER_ID,
|
|
};
|
|
|
|
const response = await saveToEndpoint('https://api.zephyrscale.smartbear.com/v2/testcycles', testCycle);
|
|
return response.data;
|
|
}
|
|
|
|
async function createTestExecutions(allTests, testCycle, options = {}) {
|
|
const {
|
|
IOS,
|
|
JIRA_PROJECT_KEY,
|
|
ZEPHYR_ENVIRONMENT_NAME,
|
|
} = process.env;
|
|
const platform = IOS === 'true' ? 'iOS' : 'Android';
|
|
const framework = options.framework || 'Detox';
|
|
|
|
const testCases = getTM4JTestCases(allTests);
|
|
const startDate = new Date(allTests.start);
|
|
const startTime = startDate.getTime();
|
|
|
|
const promises = [];
|
|
Object.entries(testCases).forEach(([key, steps], index) => {
|
|
const testScriptResults = steps.
|
|
sort((a, b) => a.title.localeCompare(b.title)).
|
|
map((item) => {
|
|
return {
|
|
title: item.title,
|
|
statusName: status[item.state],
|
|
actualEndDate: new Date(startTime + item.incrementalDuration).toISOString(),
|
|
actualResult: `${framework} automated test completed`,
|
|
};
|
|
});
|
|
|
|
const stateResult = getStepStateResult(steps);
|
|
|
|
const testExecution = {
|
|
projectKey: JIRA_PROJECT_KEY,
|
|
testCaseKey: key,
|
|
testCycleKey: testCycle.key,
|
|
statusName: stateResult.passed && stateResult.passed === steps.length ? 'Pass' : 'Fail',
|
|
testScriptResults,
|
|
environmentName: ZEPHYR_ENVIRONMENT_NAME || platform,
|
|
actualEndDate: testScriptResults[testScriptResults.length - 1].actualEndDate,
|
|
executionTime: steps.reduce((acc, prev) => {
|
|
acc += prev.duration; // eslint-disable-line no-param-reassign
|
|
return acc;
|
|
}, 0),
|
|
comment: `${framework} automated test - ${getStepStateSummary(steps)}`,
|
|
};
|
|
|
|
// Temporarily log to verify cases that were being saved.
|
|
console.log(index, key); // eslint-disable-line no-console
|
|
|
|
promises.push(saveTestExecution(testExecution, index));
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
console.log('Successfully saved test cases into the Test Management System');
|
|
}
|
|
|
|
const saveTestCases = async (allTests) => {
|
|
const {start, end} = allTests;
|
|
|
|
const testCycle = await createTestCycle(start, end);
|
|
|
|
await createTestExecutions(allTests, testCycle);
|
|
};
|
|
|
|
const RETRY = [];
|
|
|
|
async function saveTestExecution(testExecution, index) {
|
|
await axios({
|
|
method: 'POST',
|
|
url: 'https://api.zephyrscale.smartbear.com/v2/testexecutions',
|
|
headers: {
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
Authorization: process.env.ZEPHYR_API_KEY,
|
|
},
|
|
data: testExecution,
|
|
}).then(() => {
|
|
console.log(chalk.green('Success:', index, testExecution.testCaseKey));
|
|
}).catch((error) => {
|
|
// Retry on 500 error code / internal server error
|
|
if (!error.response || error.response.data.errorCode === 500) {
|
|
if (RETRY[testExecution.testCaseKey]) {
|
|
RETRY[testExecution.testCaseKey] += 1;
|
|
} else {
|
|
RETRY[testExecution.testCaseKey] = 1;
|
|
}
|
|
|
|
saveTestExecution(testExecution, index);
|
|
console.log(chalk.magenta('Retry:', index, testExecution.testCaseKey, `(${RETRY[testExecution.testCaseKey]}x)`));
|
|
} else {
|
|
console.log(chalk.red('Error:', index, testExecution.testCaseKey, error.response.data.message));
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
createTestCycle,
|
|
saveTestCases,
|
|
createTestExecutions,
|
|
};
|