mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-04 09:58:57 +00:00
* ci(e2e): make E2E/Override waive checks instead of cancelling runs E2E/Override previously cancelled in-flight E2E runs and posted success on their commit statuses. Cancelling destroys the very signal the maintainer might still want, and reporting success for a run that never finished attributes a conclusion to work that never happened. Override now means one thing only: the required e2e-test/* contexts go green so the PR can merge. Runs keep going and keep reporting to TSIO. - Add .github/actions/e2e-override-status to post success (or pending) on all five required e2e-test/* contexts for a SHA. - Label manager posts success on `labeled` and pending on `unlabeled`, so removing the escape hatch re-blocks the PR instead of leaving a stale waiver on the head SHA. It comments the runs it left running rather than killing them, and drops to `actions: read` now that it never cancels. - tsio-report-status re-reads the PR labels before posting a failure. Because runs are no longer cancelled, a result landing after the override was applied would otherwise re-red a check the maintainer deliberately bypassed. The lookup fails closed so an unreadable label list cannot turn a real failure green, and the step itself no longer fails under override. - Guard e2e-cancel-on-label-removal: stripping E2E/Run* is exactly what the Honor E2E/Override step does, so without this the indirect unlabeled event would cancel the runs the redesign is meant to preserve. - e2e-detox-pr posts the success itself when the policy skips E2E, since every downstream job is skipped and a skipped job leaves the required contexts absent, blocking the merge forever. - cancel-e2e-runs drops status_state: a cancelled run has no conclusion to report, so it always resets to pending. - Thread `issues: read` through the reusable workflow callers so finalize can read the labels; a reusable workflow cannot escalate past its caller. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): derive required E2E contexts from one definition The override and cancel actions each carried their own copy of the five required e2e-test/* contexts, kept aligned with build-tsio-job-config.js by comment alone. A job renamed in PR_MAIN_JOBS would silently leave the actions waiving or resetting a context that no longer exists, so a PR either cannot merge or merges while a real context still blocks it. Export E2E_STATUS_CONTEXTS from detox/utils/build-tsio-job-config.js, derived from PR_MAIN_JOBS, and require it from both composite actions. github-script resolves relative requires against the workspace, and every call site already checks out the repo before using the action. Also set persist-credentials: false on the e2e-label-manager checkout, matching the other checkout steps. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): drop the cancel action for native concurrency, keep GitHub calls in Actions Review feedback from @saturninoabril. Superseding an in-flight E2E run is what GitHub's `concurrency` already does, so `e2e-detox-pr.yml` now declares `concurrency: e2e-<ref>` with `cancel-in-progress` instead of cancelling from a step inside the run that supersedes it. The older run is cancelled the moment the new dispatch is created rather than once a runner picks up the first job. That removes the only two remaining callers of `.github/actions/cancel-e2e-runs`, along with the commit statuses it forced to `pending` on cancel — a cancelled run has no conclusion to report, so the state is left to GitHub. Forcing a commit status is now reserved for real test results and the override waiver. The action is deleted. `e2e-cancel-on-label-removal.yml` goes with it: with `E2E/Run` on its way out and override no longer cancelling, removing the label to stop a run has no remaining practical case, and dropping the workflow also removes the back-door cancel guard it needed. The `E2E/Override` lookup moves out of `tsio-report-status.js` into a new `.github/actions/e2e-override-label` composite action, which the finalize steps call and pass in via `--e2e-override`. Talking to GitHub stays in the Actions layer and the reporter is left deciding on its inputs. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): paginate PR label lookups for E2E/Override Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): re-post E2E/Override success on new commits Override success is SHA-scoped: it is posted only on the head SHA at label-apply time (e2e-label-manager 'labeled' path) and when Matterwick dispatches e2e-detox-pr.yml (post-e2e-override-status). On a push after E2E/Override is applied, refresh-e2e-run-label strips the run labels and returns — Matterwick is not dispatched, so the new head SHA gets no e2e-test/* statuses. With those checks Required, the new commit is blocked until the label is re-toggled. Carry the override onto the new commit: refresh-e2e-run-label now emits an e2e_override output, and when it is set a gated checkout + e2e-override-status step re-posts success on the new head SHA. Reuses the existing e2e-override-status action (same 5 required e2e-test/* contexts, same 'E2E bypassed' description), so it is idempotent with the labeled path. Adds statuses: write / contents: read to the job. Note: refresh-e2e-run-label is gated on base.ref == 'main', so stacked PRs (base != main) are not covered; non-main-base already posts success at dispatch time. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
6.2 KiB
JavaScript
179 lines
6.2 KiB
JavaScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
/* eslint-disable no-console -- CI utility script */
|
|
|
|
/**
|
|
* Build per-platform TSIO upload + GitHub commit-status config.
|
|
*
|
|
* Each Detox/Maestro platform job owns one report group (total_reports_expected=1)
|
|
* and one commit-status context, so jobs cannot clobber each other's finalize.
|
|
*/
|
|
|
|
// Commit-status namespace, aligned with the mattermost monorepo (`e2e-test/*`).
|
|
const STATUS_CONTEXT_PREFIX = 'e2e-test';
|
|
|
|
const PR_MAIN_JOBS = {
|
|
'detox-ios': {statusName: 'detox-ios', framework: 'detox'},
|
|
'detox-android': {statusName: 'detox-android', framework: 'detox'},
|
|
'detox-ipad': {statusName: 'detox-ipad', framework: 'detox'},
|
|
'maestro-ios-e2e': {statusName: 'maestro-ios', framework: 'maestro'},
|
|
'maestro-android-e2e': {statusName: 'maestro-android', framework: 'maestro'},
|
|
};
|
|
|
|
/**
|
|
* @param {string} name - unprefixed check name (e.g. detox-ios, detox-ios-Server_11.9.0)
|
|
* @returns {string} namespaced commit-status context (e.g. e2e-test/detox-ios)
|
|
*/
|
|
function statusContextFor(name) {
|
|
return `${STATUS_CONTEXT_PREFIX}/${name}`;
|
|
}
|
|
|
|
// The required contexts a PR must satisfy to merge. Single source of truth for
|
|
// .github/actions/e2e-override-status, which waives them and requires this file
|
|
// from the workspace checkout.
|
|
const E2E_STATUS_CONTEXTS = Object.values(PR_MAIN_JOBS).map((job) => statusContextFor(job.statusName));
|
|
|
|
/**
|
|
* CMT shard keys (detox-ios-Server_11.9.0, maestro-android-Server_11.9.0) are not in
|
|
* PR_MAIN_JOBS, so infer the framework from the key. Without this a Maestro shard
|
|
* inherits the base identity's `detox`, disagreeing with the `framework: maestro`
|
|
* the upload action posts for the same report group.
|
|
* @param {string} jobKey
|
|
* @returns {string|undefined}
|
|
*/
|
|
function frameworkFromJobKey(jobKey) {
|
|
if (jobKey.startsWith('maestro-')) {
|
|
return 'maestro';
|
|
}
|
|
if (jobKey.startsWith('detox-')) {
|
|
return 'detox';
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* @param {object} baseIdentity - shared fields (repository, commit_sha, branch, name prefix, …)
|
|
* @param {string} jobKey - tsio-shard-name / platform key (e.g. detox-ios, detox-ios-Server_11.9.0)
|
|
* @param {{statusName?: string, framework?: string}} [overrides]
|
|
* @returns {{composite_identity: object, total_reports_expected: number, status_context: string}}
|
|
*/
|
|
function buildTsioJobConfig(baseIdentity, jobKey, overrides = {}) {
|
|
if (!baseIdentity || typeof baseIdentity !== 'object') {
|
|
throw new Error('buildTsioJobConfig: baseIdentity is required');
|
|
}
|
|
if (!jobKey || typeof jobKey !== 'string') {
|
|
throw new Error('buildTsioJobConfig: jobKey is required');
|
|
}
|
|
|
|
const known = PR_MAIN_JOBS[jobKey];
|
|
const prefix = baseIdentity.name || baseIdentity.run_group || 'mobile-pr';
|
|
const reportName = `${prefix}-${jobKey}`;
|
|
const framework = overrides.framework || known?.framework ||
|
|
frameworkFromJobKey(jobKey) || baseIdentity.framework || 'detox';
|
|
const statusName = overrides.statusName || known?.statusName || jobKey;
|
|
|
|
return {
|
|
composite_identity: {
|
|
...baseIdentity,
|
|
name: reportName,
|
|
run_group: reportName,
|
|
framework,
|
|
},
|
|
total_reports_expected: 1,
|
|
status_context: statusContextFor(statusName),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {object} baseIdentity
|
|
* @param {string[]} jobKeys
|
|
* @returns {Record<string, ReturnType<typeof buildTsioJobConfig>>}
|
|
*/
|
|
function buildTsioJobConfigMap(baseIdentity, jobKeys) {
|
|
const out = {};
|
|
for (const key of jobKeys) {
|
|
out[key] = buildTsioJobConfig(baseIdentity, key);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Job keys that run for a given PLATFORM input (ios | android | both).
|
|
* @param {string} [platform]
|
|
* @returns {string[]}
|
|
*/
|
|
function jobKeysForPlatform(platform) {
|
|
const p = (platform || 'both').toLowerCase();
|
|
if (p === 'ios') {
|
|
return ['detox-ios', 'detox-ipad', 'maestro-ios-e2e'];
|
|
}
|
|
if (p === 'android') {
|
|
return ['detox-android', 'maestro-android-e2e'];
|
|
}
|
|
return Object.keys(PR_MAIN_JOBS);
|
|
}
|
|
|
|
// CMT shard names are `<framework>-<platform>-Server_<version>` (see
|
|
// compatibility-matrix-testing.yml tsio-shard-name inputs).
|
|
const CMT_DETOX_SHARD_PREFIXES = ['detox-ios', 'detox-ipad', 'detox-android'];
|
|
const CMT_MAESTRO_SHARD_PREFIXES = ['maestro-ios', 'maestro-android'];
|
|
|
|
/**
|
|
* CMT shard keys for the channel rollup. Detox runs against every server version in
|
|
* the matrix; Maestro only against the latest-filtered `prepare-maestro-matrix` output.
|
|
*
|
|
* @param {{server?: Array<{version?: string}>}} cmtMatrix - inputs.CMT_MATRIX
|
|
* @param {{server?: Array<{version?: string}>}} [maestroMatrix] - prepare-maestro-matrix output
|
|
* @returns {string[]}
|
|
*/
|
|
function cmtJobKeys(cmtMatrix, maestroMatrix) {
|
|
const versionsOf = (matrix) => (matrix && Array.isArray(matrix.server) ? matrix.server : []).
|
|
map((entry) => entry && entry.version).
|
|
filter(Boolean);
|
|
|
|
const keys = [];
|
|
for (const version of versionsOf(cmtMatrix)) {
|
|
for (const prefix of CMT_DETOX_SHARD_PREFIXES) {
|
|
keys.push(`${prefix}-Server_${version}`);
|
|
}
|
|
}
|
|
for (const version of versionsOf(maestroMatrix)) {
|
|
for (const prefix of CMT_MAESTRO_SHARD_PREFIXES) {
|
|
keys.push(`${prefix}-Server_${version}`);
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
/**
|
|
* Map report group name to webhook routing bucket (mobile-pr / mobile-main / mobile-release).
|
|
* @param {string} reportName
|
|
* @returns {string}
|
|
*/
|
|
function webhookBucketForReportName(reportName) {
|
|
if (!reportName || typeof reportName !== 'string') {
|
|
return '';
|
|
}
|
|
if (reportName === 'mobile-release' || reportName.startsWith('mobile-release-')) {
|
|
return 'mobile-release';
|
|
}
|
|
if (reportName === 'mobile-main' || reportName.startsWith('mobile-main-')) {
|
|
return 'mobile-main';
|
|
}
|
|
if (reportName === 'mobile-pr' || reportName.startsWith('mobile-pr-')) {
|
|
return 'mobile-pr';
|
|
}
|
|
return reportName;
|
|
}
|
|
|
|
module.exports = {
|
|
PR_MAIN_JOBS,
|
|
E2E_STATUS_CONTEXTS,
|
|
frameworkFromJobKey,
|
|
buildTsioJobConfig,
|
|
buildTsioJobConfigMap,
|
|
jobKeysForPlatform,
|
|
cmtJobKeys,
|
|
webhookBucketForReportName,
|
|
};
|