mattermost-mobile-upstream/detox/utils/tsio-report-status.js
yasser khan 16dd7dbdba
ci(e2e): make E2E/Override waive checks instead of cancelling runs (#9975)
* 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>
2026-07-31 14:22:19 +02:00

546 lines
22 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)}`;
}
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,
} = 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;
}
return `${displayReportUrl}?gid=${reportId}`;
}
function decideStatus(detail, upstreamSucceeded) {
const stats = detail.test_stats || {};
const bothTerminal = TERMINAL_STATUSES.includes(detail.status);
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),
};
}
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,
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,
} = 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);
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,
});
await postStatus({state, description, targetUrl});
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,
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',
});
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,
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);
});
}