mattermost-mobile-upstream/detox/utils/tsio-report-status.js
yasser khan 93b1863b58
Some checks are pending
ci / lint-typecheck (push) Waiting to run
ci / detox-lint-typecheck (push) Waiting to run
ci / test (push) Waiting to run
ci(e2e): triage red Detox and Maestro runs against test history (#10172)
* ci(e2e): triage red Detox and Maestro runs against test history

When a Detox or Maestro lane is red, the toolkit's e2e-triage action checks
each failure against TSIO history (flaky or broken on main, the same failure on
other PRs, environment errors, or caused by this PR) and adds its verdict to the
lane's status and the job summary. Report-only unless E2E_TRIAGE_MODE is enforce.

- .github/actions/e2e-triage: the per-lane triage steps (evidence download,
  answers cache, toolkit e2e-triage) as one composite, checked out from the
  dispatch ref so the branch under test never runs with the API key.
- Each lane uploads what it saw at a failure (screenshots, the step it
  stopped at, the server's answer) as triage evidence.
- The TSIO status links name the run only when both the run id and the
  attempt are known, so triage writes only on statuses its run owns.

Toolkit pinned to bdce26e.

* ci(e2e): pin the toolkit to 24bf208; use the renamed e2e-test-* actions

The toolkit renamed e2e-triage to e2e-test-triage and e2e-status to
e2e-test-status; inputs are unchanged. Triage now holds a failure repeated on
this PR's last commits and never on the base branch as out of date (merge the
base branch and re-run) instead of clearing it, and the status line names each
kind: flaky, broken on main, caused by PR, out of date, needs investigation.

* ci(e2e): pin the toolkit to main (419b0a9)

Toolkit #5 merged; this pins its merge on main instead of the PR branch.

* ci(e2e): pin the toolkit to main (b3ca677)

Toolkit #6 merged: a run flagged as a whole now says why once instead of repeating "investigation required".

* ci(e2e): pin the toolkit to e2e-triage-judge-flagged-runs (f1f5d54, toolkit#8)

* ci(e2e): pin the toolkit to e2e-triage-judge-flagged-runs (77c3048, toolkit#8)

* ci(e2e): use the toolkit's main while triage is observed

toolkit#8 merged (b7757e9). Following review, consumers track main so
toolkit changes need no bump here; pin a hash again once stable.

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-10-26 10:26:45 +05:30

634 lines
25 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console, no-process-env, no-await-in-loop, curly, brace-style, max-statements-per-line, no-empty, no-negated-condition, multiline-ternary -- CI utility script */
// Posts one GitHub commit status from a TSIO report group (poll + display URL).
const PRODUCTION_URL = 'https://test-io.test.mattermost.com';
const STAGING_URL = 'https://staging-test-io.test.mattermost.com';
const OVERRIDE_LABEL = 'E2E/Override';
const TERMINAL_STATUSES = ['completed', 'incomplete'];
const DEFAULT_POLL_ATTEMPTS = 12;
const POLL_DELAY_MS = 5000;
const FETCH_TIMEOUT_MS = 30000;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, {...options, signal: controller.signal});
} finally {
clearTimeout(timer);
}
}
function repoTail(repository) {
return (repository || '').split('/').pop() || repository;
}
function buildDisplayReportUrl(baseUrl, identity) {
const repoTrailing = repoTail(identity.repository);
const rawBranch = identity.branch || 'main';
const branch = rawBranch.replace(/^refs\/heads\//, '').replace(/^refs\/tags\//, '').replace(/\//g, '~');
const shortSha = (identity.commit_sha || '').slice(0, 7);
// Prefer producer-declared run_group (TSIO consolidates by this), else name.
const name = identity.run_group || identity.name || 'mobile-pr';
return `${baseUrl}/reports/${encodeURIComponent(repoTrailing)}/${encodeURIComponent(branch)}/${shortSha}/${encodeURIComponent(name)}`;
}
// The run posting the status. Its id and attempt go into the status link, where the toolkit's
// e2e-status reads who owns the status: without them triage can't add its note to the check.
function statusRun(identity) {
return {
id: String(process.env.GITHUB_RUN_ID || identity.gh_run_id || ''),
attempt: String(process.env.GITHUB_RUN_ATTEMPT || identity.gh_run_attempt || ''),
};
}
function buildWorkflowRunUrl(identity) {
const server = process.env.GITHUB_SERVER_URL || 'https://github.com';
const repository = process.env.GITHUB_REPOSITORY || identity.repository;
const runId = process.env.GITHUB_RUN_ID || identity.gh_run_id || '';
return `${server}/${repository}/actions/runs/${runId}`;
}
// Commit-status target_url policy:
// - Terminal TSIO group with a usable report → TSIO (success, test failures, or incomplete).
// - Clean completed report but CI failed outside tests (upstreamSucceeded=false) → workflow run.
// - Non-terminal / missing report (OIDC/begin/poll callers use runUrl separately) → workflow run.
function decideTargetUrl(state, bothTerminal, displayReportUrl, reportId, runUrl, opts = {}) {
const {
failed = 0,
reportStatus,
upstreamSucceeded = true,
run,
} = opts;
if (!bothTerminal || !reportId || !displayReportUrl) {
return runUrl;
}
const hasTestFailures = (failed || 0) > 0;
const unrelatedCiFailure =
state === 'failure' &&
!hasTestFailures &&
reportStatus === 'completed' &&
upstreamSucceeded === false;
if (unrelatedCiFailure) {
return runUrl;
}
const url = new URL(displayReportUrl);
url.searchParams.set('gid', reportId);
if (run?.id && run?.attempt) {
url.searchParams.set('gh_run_id', run.id);
url.searchParams.set('gh_run_attempt', run.attempt);
}
return url.href;
}
function decideStatus(detail, upstreamSucceeded, cancelled = false) {
const stats = detail.test_stats || {};
const bothTerminal = TERMINAL_STATUSES.includes(detail.status);
// Specs a cancelled run never finished are counted as failed, and its server can
// be torn down under the steps that still run: none of that says anything about
// the change. Report the cancel, not failures.
if (cancelled) {
return {
state: 'error',
description: `E2E run cancelled before it finished (${stats.passed || 0} passed by then) · re-run E2E`.slice(0, 140),
both_terminal: bothTerminal,
timed_out: false,
};
}
if (bothTerminal) {
const clean = (stats.failed || 0) === 0 && detail.status === 'completed' && upstreamSucceeded;
const passed = stats.passed || 0;
const failed = stats.failed || 0;
const skipped = stats.skipped || 0;
const state = clean ? 'success' : 'failure';
let description;
if (failed > 0) {
description = `${passed} passed, ${failed} failed, ${skipped} skipped`;
} else if (detail.status !== 'completed') {
description = `Report ${detail.status} — ${passed} passed, ${skipped} skipped`;
} else {
description = `${passed} passed, ${skipped} skipped`;
}
return {state, description: description.slice(0, 140), both_terminal: true, timed_out: false};
}
return {
state: upstreamSucceeded ? 'success' : 'failure',
description: `TSIO poll timed out (status=${detail.status}) — using CI job status`.slice(0, 140),
both_terminal: false,
timed_out: true,
};
}
const {parseArgs} = require('./cli-args');
async function mintOidcToken(audience) {
const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
const bearer = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (!url || !bearer) {
throw new Error('OIDC request env vars not set — job needs permissions.id-token: write');
}
const sep = url.includes('?') ? '&' : '?';
const res = await fetchWithTimeout(`${url}${sep}audience=${encodeURIComponent(audience)}`, {
headers: {Authorization: `bearer ${bearer}`, Accept: 'application/json; api-version=2.0'},
});
if (!res.ok) throw new Error(`OIDC mint failed: ${res.status} ${await res.text()}`);
const body = await res.json();
if (!body.value) throw new Error('OIDC mint returned no token');
return body.value;
}
async function beginGroup(baseUrl, idToken, identity, totalReportsExpected) {
const body = {
repository: identity.repository,
commit: identity.commit_sha,
gh_run_id: identity.gh_run_id,
gh_run_attempt: String(identity.gh_run_attempt),
framework: identity.framework || 'detox',
name: identity.name,
branch: identity.branch,
total_reports_expected: totalReportsExpected,
...(identity.run_group ? {run_group: identity.run_group} : {}),
...(identity.gh_pr_number != null ? {gh_pr_number: parseInt(identity.gh_pr_number, 10)} : {}),
};
const res = await fetchWithTimeout(`${baseUrl}/api/v1/reports/begin`, {
method: 'POST',
headers: {'Content-Type': 'application/json', Authorization: `Bearer ${idToken}`},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`reports/begin (${identity.name}) failed: ${res.status} ${await res.text()}`);
const {report_id} = await res.json();
if (!report_id) throw new Error(`reports/begin (${identity.name}) returned no report_id`);
return report_id;
}
async function pollGroup(baseUrl, reportId, pollAttempts) {
const attempts = Number.isFinite(pollAttempts) && pollAttempts >= 1 ?
pollAttempts :
DEFAULT_POLL_ATTEMPTS;
let detail = {status: 'unknown', test_stats: {}};
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const res = await fetchWithTimeout(`${baseUrl}/api/v1/reports/${reportId}`);
if (!res.ok) throw new Error(`reports/${reportId} failed: ${res.status} ${await res.text()}`);
detail = await res.json();
if (TERMINAL_STATUSES.includes(detail.status)) return detail;
} catch (err) {
console.error(`tsio-report-status (poll attempt ${attempt + 1}/${attempts}):`, err.message);
if (attempt >= attempts - 1) throw err;
}
if (attempt < attempts - 1) await sleep(POLL_DELAY_MS);
}
return detail;
}
// A maintainer applying E2E/Override means the PR is meant to merge without E2E
// results. E2E runs are not cancelled when the label lands, so a run that started
// before the override must not re-red the required check when it finishes.
// Whether the label is set is resolved by .github/actions/e2e-override-label and
// passed in, so this stays a pure decision on its inputs.
function overrideCommitStatus(state, description) {
if (state !== 'failure') {
return {state, description};
}
return {
state: 'success',
description: `${OVERRIDE_LABEL} — ${description}`.slice(0, 140),
};
}
// The PR-wide triage check. Every lane's triage job writes it too (the
// e2e-triage action's triage-status-context), with the verdict for all lanes.
const TRIAGE_CHECK_CONTEXT = 'e2e-test/triage';
/**
* The pending check that tells a PR its red lanes are waiting for E2E triage.
* Triage (the e2e-triage action in mattermost-test-automation-toolkit) runs
* only once every lane has finished, which can be an hour after this one, and
* then writes the same check with its verdict. Null when there is nothing to
* announce: triage is off for this run, the lane isn't red with test failures,
* or E2E/Override already turned it green.
*
* @returns {{state: string, context: string, description: string, target_url: string} | null}
*/
function triageAnnouncement({announce, state, overrideApplied, failed, runUrl, cancelled = false}) {
// A cancelled run is not triaged (e2e-detox-pr.yml skips tsio-triage), so the
// check says so instead of waiting for a verdict that will never come.
if (announce && cancelled) {
return {
state: 'error',
context: TRIAGE_CHECK_CONTEXT,
description: 'E2E run cancelled before triage · re-run E2E',
target_url: runUrl,
};
}
if (!announce || state !== 'failure' || overrideApplied || !(failed > 0)) {
return null;
}
return {
state: 'pending',
context: TRIAGE_CHECK_CONTEXT,
description: 'E2E failures found · triage starts when every lane has finished',
target_url: runUrl,
};
}
async function createCommitStatus(token, repository, sha, payload) {
const [owner, repo] = repository.split('/');
const res = await fetchWithTimeout(`https://api.github.com/repos/${owner}/${repo}/statuses/${sha}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`createCommitStatus failed: ${res.status} ${await res.text()}`);
}
async function reportTsioStatus(options) {
const {
compositeIdentity,
totalReportsExpected,
commitStatusContext,
upstreamJobsSucceeded = true,
cancelled = false,
githubToken,
e2eOverride = false,
failOnTestFailures = true,
pollAttempts = DEFAULT_POLL_ATTEMPTS,
useStaging = false,
audience = 'mattermost-test-system-io',
baseUrl: baseUrlOverride,
// Per-job finalize leaves this false so N platform legs do not spam the channel.
channelNotify = false,
announceTriage = false,
} = options;
const baseUrl = baseUrlOverride || (useStaging ? STAGING_URL : PRODUCTION_URL);
const runUrl = buildWorkflowRunUrl(compositeIdentity);
const result = {
context: commitStatusContext,
tsio_url: baseUrl,
upstream_succeeded: upstreamJobsSucceeded,
report_id: null,
group_url: null,
display_report_url: buildDisplayReportUrl(baseUrl, compositeIdentity),
status: null,
test_stats: {},
state: 'failure',
timed_out: false,
override_applied: false,
};
const token = githubToken || process.env.GITHUB_TOKEN;
if (!token) throw new Error('githubToken (or GITHUB_TOKEN env) is required');
const postStatus = async ({state, description, targetUrl}) => {
let final = {state, description};
if (e2eOverride && state === 'failure') {
final = overrideCommitStatus(state, description);
result.override_applied = true;
console.log(`${OVERRIDE_LABEL} set on PR #${compositeIdentity.gh_pr_number} — posting success for ${commitStatusContext}`);
}
result.state = final.state;
await createCommitStatus(token, compositeIdentity.repository, compositeIdentity.commit_sha, {
state: final.state,
context: commitStatusContext,
description: final.description,
target_url: targetUrl,
});
return final.state;
};
let idToken;
try {
idToken = await mintOidcToken(audience);
} catch (err) {
console.error('tsio-report-status (OIDC):', err.message);
await postStatus({
state: 'failure',
description: `TSIO error: ${err.message}`.slice(0, 140),
targetUrl: runUrl,
});
result.error = err.message;
return result;
}
let reportId;
try {
reportId = await beginGroup(baseUrl, idToken, compositeIdentity, totalReportsExpected);
} catch (err) {
console.error('tsio-report-status (begin):', err.message);
await postStatus({
state: 'failure',
description: `TSIO begin failed: ${err.message}`.slice(0, 140),
targetUrl: runUrl,
});
result.error = err.message;
return result;
}
result.report_id = reportId;
result.group_url = `${baseUrl}/reports/g/${reportId}`;
let detail;
try {
detail = await pollGroup(baseUrl, reportId, pollAttempts);
} catch (err) {
console.error('tsio-report-status (poll):', err.message);
await postStatus({
state: 'failure',
description: `TSIO poll failed: ${err.message}`.slice(0, 140),
targetUrl: runUrl,
});
result.error = err.message;
return result;
}
result.status = detail.status;
result.test_stats = detail.test_stats || {};
const {state, description, both_terminal: bothTerminal, timed_out: timedOut} =
decideStatus(detail, upstreamJobsSucceeded, cancelled);
result.timed_out = timedOut;
result.state = state;
const targetUrl = decideTargetUrl(state, bothTerminal, result.display_report_url, reportId, runUrl, {
failed: result.test_stats.failed || 0,
reportStatus: detail.status,
upstreamSucceeded: upstreamJobsSucceeded,
run: statusRun(compositeIdentity),
});
await postStatus({state, description, targetUrl});
const announcement = triageAnnouncement({
announce: announceTriage,
state: result.state,
overrideApplied: result.override_applied,
failed: result.test_stats.failed || 0,
context: commitStatusContext,
runUrl,
cancelled,
});
if (announcement) {
// Best effort: the required status above is what gates the PR.
try {
await createCommitStatus(token, compositeIdentity.repository, compositeIdentity.commit_sha, announcement);
} catch (err) {
console.warn(`tsio-report-status: triage check not posted: ${err.message}`);
}
}
if (process.env.GITHUB_STEP_SUMMARY) {
const stats = result.test_stats;
const lines = [
`### TSIO status — ${commitStatusContext}`,
'',
`**State:** ${result.state}`,
`**Display report:** [${compositeIdentity.name}](${result.display_report_url})`,
`**Group:** [${reportId}](${result.group_url}) — ${result.status}`,
`**Stats:** ${stats.passed || 0} passed, ${stats.failed || 0} failed, ${stats.skipped || 0} skipped (of ${stats.total || 0})`,
...(timedOut ? ['', ':warning: TSIO group did not reach terminal status in time — failing open to CI job status.'] : []),
...(result.override_applied ? ['', `:warning: ${OVERRIDE_LABEL} is set on this PR — reporting ${state} as success so the required check does not block the merge.`] : []),
];
try {
const fs = require('fs');
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, lines.join('\n') + '\n');
} catch {}
}
// Channel notify (best-effort). Routing (see resolveWebhookUrl):
// mobile-release[-*] → MATTERMOST_CMT_WEBHOOK_URL
// mobile-main[-*] → MATTERMOST_MASTER_HEALTH_WEBHOOK_URL
// mobile-pr[-*] → MATTERMOST_E2E_WEBHOOK_URL
// Failures here must not undo a successfully written commit status.
// Opt in with channelNotify / CHANNEL_NOTIFY=true (summary job or explicit).
try {
const shouldNotify = channelNotify === true || process.env.CHANNEL_NOTIFY === 'true';
const {webhookBucketForReportName} = require('./build-tsio-job-config');
const reportName = compositeIdentity.run_group || compositeIdentity.name;
const bucket = webhookBucketForReportName(reportName) ||
webhookBucketForReportName(compositeIdentity.name);
if (shouldNotify && (bucket === 'mobile-release' || bucket === 'mobile-pr' || bucket === 'mobile-main')) {
const {notifyCmtChannel, resolveWebhookUrl} = require('./cmt-channel-notify.js');
const webhookUrl = resolveWebhookUrl(compositeIdentity.name);
if (webhookUrl) {
const failedShards = (detail.reports || []).
filter((r) => r.status && r.status !== 'complete' && r.status !== 'completed').
map((r) => r.gh_job_name || r.display_name || r.id).
filter(Boolean);
const hasFailures = (result.test_stats.failed || 0) > 0 || failedShards.length > 0;
const channelReportUrl = result.display_report_url || result.group_url || targetUrl;
const core = {
info: (msg) => console.log(msg),
warning: (msg) => console.warn(msg),
};
await notifyCmtChannel({
core,
baseUrl,
compositeIdentity,
detail,
reportUrl: channelReportUrl,
upstreamJobsSucceeded,
hasFailures,
webhookUrl,
});
}
}
} catch (error) {
console.warn(`E2E Mattermost notify setup failed: ${error.message}`);
}
// Under override the commit status is green; failing the step too would only
// turn the run red again for a result the maintainer chose to waive.
if (failOnTestFailures && !result.override_applied && bothTerminal && (result.test_stats.failed || 0) > 0) {
const err = new Error('TSIO reported test failures');
err.exitCode = 1;
throw err;
}
return result;
}
function selfTest() {
const assert = (cond, msg) => {
if (!cond) { console.error('SELF-TEST FAIL:', msg); process.exit(1); }
};
const url = buildDisplayReportUrl(PRODUCTION_URL, {
repository: 'mattermost/mattermost-mobile',
commit_sha: 'abc1234deadbeef',
branch: 'feature-branch',
name: 'mobile-pr',
run_group: 'mobile-pr',
});
assert(url === 'https://test-io.test.mattermost.com/reports/mattermost-mobile/feature-branch/abc1234/mobile-pr', `display URL: ${url}`);
const urlSlashBranch = buildDisplayReportUrl(PRODUCTION_URL, {
repository: 'mattermost/mattermost-mobile',
commit_sha: 'abc1234deadbeef',
branch: 'feat/tsio-mobile-reporting',
name: 'mobile-pr',
run_group: 'mobile-pr',
});
assert(urlSlashBranch === 'https://test-io.test.mattermost.com/reports/mattermost-mobile/feat~tsio-mobile-reporting/abc1234/mobile-pr', `slash branch URL: ${urlSlashBranch}`);
const urlMain = buildDisplayReportUrl(PRODUCTION_URL, {
repository: 'mattermost/mattermost-mobile',
commit_sha: 'def5678',
branch: 'refs/heads/main',
name: 'mobile-main',
run_group: 'mobile-main',
});
assert(urlMain === 'https://test-io.test.mattermost.com/reports/mattermost-mobile/main/def5678/mobile-main', `main URL: ${urlMain}`);
const cmtUrl = buildDisplayReportUrl(PRODUCTION_URL, {
repository: 'mattermost/mattermost-mobile',
commit_sha: '111aaaa',
branch: 'release-2.40',
name: 'mobile-release',
run_group: 'mobile-release',
});
assert(cmtUrl === 'https://test-io.test.mattermost.com/reports/mattermost-mobile/release-2.40/111aaaa/mobile-release', `cmt URL: ${cmtUrl}`);
let r = decideStatus({status: 'completed', test_stats: {passed: 10, failed: 0, skipped: 1}}, true);
assert(r.state === 'success', 'clean run -> success');
r = decideStatus({status: 'completed', test_stats: {passed: 8, failed: 2, skipped: 0}}, true);
assert(r.state === 'failure', 'failures -> failure');
r = decideStatus({status: 'in_progress', test_stats: {passed: 3, failed: 0}}, true);
assert(r.timed_out === true && r.state === 'success', 'timeout fail-open upstream ok');
const display = 'https://test-io.test.mattermost.com/reports/mattermost-mobile/main/abc1234/mobile-pr';
const actions = 'https://github.com/o/r/actions/runs/99';
assert(
decideTargetUrl('success', true, display, 'gid-1', actions, {failed: 0, reportStatus: 'completed'}) ===
`${display}?gid=gid-1`,
'success -> TSIO report URL',
);
assert(
decideTargetUrl('failure', true, display, 'gid-1', actions, {failed: 2, reportStatus: 'completed'}) ===
`${display}?gid=gid-1`,
'test failures -> TSIO report URL',
);
assert(
decideTargetUrl('failure', true, display, 'gid-1', actions, {
failed: 0,
reportStatus: 'completed',
upstreamSucceeded: false,
}) === actions,
'unrelated CI failure with clean report -> workflow run URL',
);
assert(
decideTargetUrl('failure', true, display, 'gid-1', actions, {failed: 0, reportStatus: 'incomplete'}) ===
`${display}?gid=gid-1`,
'incomplete report -> TSIO report URL',
);
assert(
decideTargetUrl('success', false, display, 'gid-1', actions) === actions,
'non-terminal -> workflow run URL',
);
const overridden = overrideCommitStatus('failure', '8 passed, 2 failed, 0 skipped');
assert(overridden.state === 'success', 'override downgrades failure -> success');
assert(overridden.description.startsWith(OVERRIDE_LABEL), 'override description is labelled');
assert(
overrideCommitStatus('success', '10 passed').description === '10 passed',
'override leaves success untouched',
);
console.log('SELF-TEST OK');
}
async function main() {
const args = parseArgs(process.argv);
if (args['self-test'] !== undefined) { selfTest(); return; }
let identity;
try {
identity = JSON.parse(args['composite-identity']);
} catch (err) {
console.error('tsio-report-status: invalid --composite-identity JSON:', err.message);
process.exit(1);
}
const totalReportsExpected = parseInt(args['total-reports-expected'] || '1', 10);
const context = args.context;
if (!context) {
console.error('tsio-report-status: --context is required (e.g. e2e-test/detox-ios)');
process.exit(1);
}
const upstreamSucceeded = args['upstream-succeeded'] !== 'false';
const failOnTestFailures = args['fail-on-test-failures'] !== 'false';
const pollAttempts = parseInt(args['poll-attempts'] || String(DEFAULT_POLL_ATTEMPTS), 10);
try {
const result = await reportTsioStatus({
compositeIdentity: identity,
totalReportsExpected,
commitStatusContext: context,
upstreamJobsSucceeded: upstreamSucceeded,
cancelled: args.cancelled === 'true',
githubToken: args['github-token'] || process.env.GITHUB_TOKEN,
e2eOverride: args['e2e-override'] === 'true',
failOnTestFailures,
pollAttempts,
useStaging: args['use-staging'] === 'true',
audience: args.audience || 'mattermost-test-system-io',
baseUrl: args['base-url'],
channelNotify: args['channel-notify'] === 'true',
announceTriage: args['announce-triage'] === 'true',
});
console.log(JSON.stringify(result, null, 2));
process.exit(0);
} catch (err) {
if (err.exitCode === 1) {
console.error('tsio-report-status:', err.message);
process.exit(1);
}
console.error('tsio-report-status fatal:', err.message);
process.exit(1);
}
}
module.exports = {
buildDisplayReportUrl,
buildWorkflowRunUrl,
decideStatus,
decideTargetUrl,
overrideCommitStatus,
reportTsioStatus,
repoTail,
triageAnnouncement,
mintOidcToken,
beginGroup,
pollGroup,
OVERRIDE_LABEL,
PRODUCTION_URL,
STAGING_URL,
};
if (require.main === module) {
main().catch((err) => {
console.error('tsio-report-status fatal:', err.message);
process.exit(1);
});
}