mattermost-mobile-upstream/.github/workflows/e2e-label-manager.yml
yasser khan 587e15ea89
chore(e2e): Cache app builds. Skip E2E on docs only PR. (#9930)
Based on the later commits (after the earlier experimentation and reversions), the meaningful end state of the PR is:

**Summary:** Improve E2E CI reliability, performance, and security by introducing robust build caching with safe fingerprinting and cache isolation, duration-based test sharding with graceful fallbacks, more reliable E2E impact detection and status handling, stronger cancellation/concurrency behavior, multiple iOS Detox stability fixes, and hardening GitHub Actions to avoid executing PR-authored code. Also includes CI fixes for iOS builds and a temporary Android E2E test skip for a known regression.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-08-04 19:13:39 +05:30

700 lines
34 KiB
YAML

# Matterwick triggers E2E only on pull_request:labeled (see matterwick/server/pull_request.go).
# synchronize alone does nothing — on push we must cancel stale runs, remove E2E/Run*,
# and re-add E2E/Run so Matterwick receives a fresh labeled webhook.
#
# 1. detect-e2e-impact (opened|synchronize):
# Path-filter; skip E2E when the PR has no app-impacting files.
#
# 2. cancel-stale-e2e-on-push (synchronize):
# Force-cancel in-flight E2E for the previous commit (even on docs-only pushes).
# No-op for E2E/Override PRs on the same push — see refresh-e2e-run-label below.
#
# 3. refresh-e2e-run-label (synchronize, app-impacting):
# Remove + re-add E2E/Run via PAT for Matterwick.
# When E2E/Override is set it instead only strips the run labels (no dispatch)
# and hands off to repost-override-status, which re-posts the bypass on the
# new head SHA — commit statuses are keyed by SHA, so without that re-post the
# pushed commit has no e2e-test/* statuses and required checks would block the
# PR. The re-post is a separate job so PR-authored action code never shares a
# runner with the PAT.
#
# 4. add-e2e-run-label (opened, app-impacting):
# Add E2E/Run unless already present. Skips when E2E/Override is set.
#
# 5. Honor E2E/Override (labeled E2E/Override):
# Removes all E2E/Run* labels so no new run is dispatched, cancels the
# in-flight run, then marks success on the required e2e-test/* contexts that
# are actually blocking — pending, failing, or with no status yet. A platform
# that is genuinely green is left with its own result, and a failing one keeps
# its pass/fail counts plus an "- e2e overridden" suffix, so the checks list
# still shows what really happened.
# The run is cancelled because merging the PR triggers Matterwick's `closed`
# cleanup, which destroys the test servers mid-run — the surviving run would
# report infra teardown as a test failure into TSIO's per-spec history. A
# late post from the cancelled run cannot re-red the check anyway: its
# finalize step re-checks the label (see .github/actions/e2e-override-label)
# and tsio-report-status converts failure → success while it is set.
#
# 6. Honor E2E/Verified (labeled/unlabeled E2E/Verified):
# A maintainer applies this after reading a real E2E failure and judging it
# known flake, not a regression. Marks only the currently-failing e2e-test/*
# contexts success, keeping the real pass/fail description and tagging who
# verified it — passing platforms are left untouched. Removing the label
# resets exactly the contexts this action touched (matched by a marker in
# the description) back to pending, so the waiver never outlives the label
# and never clobbers a platform it didn't verify. Does not carry across a
# push: a new commit gets a fresh run and a fresh human judgment, unlike
# E2E/Override.
#
# Fork PR guard (github.event.pull_request.head.repo.full_name == github.repository)
# is applied per-step. Fork PR contexts only get a read-only GITHUB_TOKEN, so
# addLabels / removeLabel / run cancellation would all 403. Mandatory E2E for fork
# PRs is enforced separately.
#
# Base-branch guard: auto E2E/Run refresh only runs for PRs targeting main.
# Stacked PRs (base != main) are skipped so Matterwick is not triggered on every
# intermediate-branch push.
#
name: E2E label manager
on:
pull_request:
types:
- opened
- synchronize
- labeled
- unlabeled
concurrency:
group: e2e-label-manager-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
# Skip E2E when PR has no app-impacting files.
detect-e2e-impact:
if: >-
(github.event.action == 'synchronize' || github.event.action == 'opened')
&& github.event.pull_request.base.ref == 'main'
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: read
outputs:
app_impacting: ${{ steps.filter.outputs.app_impacting }}
steps:
# Need the PR's paths.json so fingerprint + impact filter share one list.
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
sparse-checkout: |
.github/actions/compute-e2e-build-fingerprint
# Enterprise action allowlist blocks dorny/paths-filter; use github-script
# (allowlisted) + pulls.listFiles. Same semantics as dorny on pull_request:
# compare the FULL PR file list vs base, not only the latest push.
# Patterns come from compute-e2e-build-fingerprint/paths.json (not inlined)
# so they cannot drift from the S3 build fingerprint.
- name: Detect app-impacting paths
id: filter
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const detect = async () => {
const fs = require('fs');
const path = require('path');
// paths.json is read as DATA, never require()d, and paths.js is never
// loaded here. Everything under the sparse checkout comes from the PR
// head, so require()ing any of it would execute PR-authored JavaScript
// on the runner — arbitrary code with this job's authenticated API
// client. JSON.parse cannot execute. (The job is already restricted to
// same-repo PRs by the head.repo check above, so this is defence in
// depth rather than the only barrier.)
const paths = JSON.parse(fs.readFileSync(
path.join(
process.cwd(),
'.github/actions/compute-e2e-build-fingerprint/paths.json',
),
'utf8',
));
// Mirrors impactGlobs() in compute-e2e-build-fingerprint/paths.js —
// the single rule is "a git ls-tree dir entry (trailing /) becomes
// dir/**". paths.json stays the one source of truth for the list
// itself, which is the drift that actually matters; paths.test.js
// covers the same conversion on the action side.
const toGlob = (entry) => (entry.endsWith('/') ? `${entry}**` : entry);
const fingerprintGlobs = [
...paths.fingerprintShared,
...Object.values(paths.fingerprintByPlatform).flat(),
].map(toGlob);
// Hardcoded, NOT read from paths.json: the pattern list itself comes
// from the PR head, so a PR that trimmed its own paths.json could
// otherwise make that edit look non-impacting and skip E2E.
const ALWAYS_IMPACTING = ['.github/actions/compute-e2e-build-fingerprint/**'];
const patterns = [...new Set([...ALWAYS_IMPACTING, ...fingerprintGlobs, ...paths.triggerExtra])];
// Supports the subset used in paths.json: `**` spans path separators,
// a single `*` does not, everything else is literal. Not a general
// glob engine — brace expansion, `?` and negation are not implemented
// and would match literally, so keep paths.json to that subset.
const globToRegExp = (pattern) => {
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*\*/g, '@@GLOBSTAR@@')
.replace(/\*/g, '[^/]*')
.replace(/@@GLOBSTAR@@/g, '.*');
return new RegExp(`^${escaped}$`);
};
const matchers = patterns.map(globToRegExp);
const matches = (filePath) => matchers.some((re) => re.test(filePath));
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
per_page: 100,
});
const hit = files.find((f) => matches(f.filename) || (f.previous_filename && matches(f.previous_filename)));
return {appImpacting: Boolean(hit), hit, count: files.length};
};
const prNumber = context.payload.pull_request.number;
let result;
try {
result = await detect();
core.info(`PR #${prNumber}: ${result.count} file(s), app_impacting=${result.appImpacting}${result.hit ? ` (hit: ${result.hit.filename})` : ''}`);
} catch (err) {
// Fail closed by running E2E, and do NOT rethrow. Throwing fails this
// job, which gates both the run-E2E path and the skip-status path — the
// PR would then sit with the required e2e-test/* contexts unreported and
// no self-service fix. Treating an unreadable file list as impacting
// costs one E2E run; the alternative silently ships untested code or
// wedges the PR. Realistic trigger is a pulls.listFiles hiccup, since a
// malformed paths.json is caught by paths.test.js before merge.
core.warning(`Impact filter failed, running E2E to be safe: ${err.message}`);
result = {appImpacting: true};
}
core.setOutput('app_impacting', result.appImpacting ? 'true' : 'false');
# Always cancel superseded runs on synchronize — including docs-only pushes —
# so a prior app-impacting run does not keep burning minutes after skip statuses.
# This is NOT covered by e2e-detox-pr.yml's `concurrency:` group: that group only
# fires when a NEW dispatch supersedes an old one, and a docs-only push here
# dispatches nothing at all (post-skip-status handles it instead) — so a prior
# app-impacting run would otherwise run to completion for a result nobody reads.
#
# Inlined rather than reusing a composite action: E2E/Override's cancel-on-apply
# path (.github/actions/cancel-e2e-runs) was removed when Override moved to a
# never-cancel model, and this is now the only remaining caller of the
# "force-cancel stale runs for a branch" behavior. The removed action's
# default-branch guard (no pr_number + branch == default) is not reproduced here
# because it is unreachable for this caller: this job only runs on `pull_request`
# synchronize, which always carries a PR number.
cancel-stale-e2e-on-push:
needs: detect-e2e-impact
if: >-
always()
&& needs.detect-e2e-impact.result == 'success'
&& github.event.action == 'synchronize'
&& github.event.pull_request.base.ref == 'main'
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
permissions:
actions: write
statuses: write
steps:
- name: Force-cancel in-flight E2E runs for the previous commit
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request.number;
const headBranch = context.payload.pull_request.head.ref;
const excludeHeadSha = context.payload.pull_request.head.sha;
const reason = 'E2E superseded by new commit push';
const ACTIVE_RUN_STATUSES = ['in_progress', 'queued', 'waiting'];
const E2E_STATUS_CONTEXTS = [
'e2e-test/detox-ios',
'e2e-test/detox-android',
'e2e-test/detox-ipad',
'e2e-test/maestro-ios',
'e2e-test/maestro-android',
];
const {data: workflows} = await github.rest.actions.listRepoWorkflows({owner, repo});
const e2eWorkflow = workflows.workflows.find((workflow) => workflow.name === 'E2E');
if (!e2eWorkflow) {
core.info('No E2E workflow found in repository');
return;
}
const runsToCancel = new Map();
for (const status of ACTIVE_RUN_STATUSES) {
const {data} = await github.rest.actions.listWorkflowRuns({
owner, repo, workflow_id: e2eWorkflow.id, status, branch: headBranch,
});
for (const run of data.workflow_runs) {
const matchesPr = !run.pull_requests?.length ||
run.pull_requests.some((pr) => pr.number === prNumber);
if (!matchesPr) {
core.info(`Skipping run ${run.id}: not associated with PR #${prNumber}`);
continue;
}
if (run.head_sha === excludeHeadSha) {
core.info(`Skipping run ${run.id}: already on current commit ${excludeHeadSha}`);
continue;
}
runsToCancel.set(run.id, run);
}
}
if (runsToCancel.size === 0) {
core.info(`No active E2E runs found for PR #${prNumber} on branch ${headBranch}`);
return;
}
const shasToUpdate = new Set();
for (const run of runsToCancel.values()) {
core.info(`Cancelling E2E run ${run.id} (${run.status}, sha ${run.head_sha})`);
try {
await github.rest.actions.cancelWorkflowRun({owner, repo, run_id: run.id});
// The plain cancel above is what stops the expensive build jobs —
// they are `if: !cancelled()`, so cancelling the run ends them.
// What it does NOT stop is anything gated on `always()`, which
// keeps running through a cancellation (tsio-channel-notify, and
// the artifact-upload steps inside the build jobs). Run-level
// force-cancel stops those too.
await github.request('POST /repos/{owner}/{repo}/actions/runs/{run_id}/force-cancel', {
owner, repo, run_id: run.id,
});
shasToUpdate.add(run.head_sha);
} catch (err) {
core.warning(`Cancelling run ${run.id}: ${err.message}`);
}
}
// Only reset statuses for runs actually stopped — a run still going
// will keep publishing its own results.
for (const sha of shasToUpdate) {
for (const statusContext of E2E_STATUS_CONTEXTS) {
try {
await github.rest.repos.createCommitStatus({
owner, repo, sha, state: 'pending', context: statusContext,
description: reason.substring(0, 140),
});
} catch (err) {
core.warning(`createCommitStatus ${statusContext} on ${sha}: ${err.message}`);
}
}
}
refresh-e2e-run-label:
needs:
- detect-e2e-impact
- cancel-stale-e2e-on-push
if: >-
always()
&& needs.detect-e2e-impact.result == 'success'
&& needs.detect-e2e-impact.outputs.app_impacting == 'true'
&& github.event.action == 'synchronize'
&& github.event.pull_request.base.ref == 'main'
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
outputs:
e2e_override: ${{ steps.refresh.outputs.e2e_override }}
steps:
# cancel-stale-e2e-on-push (a `needs` of this job) already force-cancels the
# previous commit's run. Repeating it here would double-cancel and post the
# commit status twice. Nothing in this job's script reads repo files, so
# there is no checkout step.
- name: Remove and re-add E2E/Run for Matterwick
id: refresh
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request.number;
const runLabels = ['E2E/Run', 'E2E/Run-iOS', 'E2E/Run-Android'];
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner, repo, issue_number: prNumber,
});
const labelNames = labels.map((label) => label.name);
if (labelNames.includes('E2E/Override')) {
core.info(`PR #${prNumber} has E2E/Override — stripping run labels only.`);
for (const label of runLabels) {
if (!labelNames.includes(label)) {
continue;
}
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: label,
});
} catch (err) {
if (err.status !== 404) {
throw err;
}
}
}
// Signal the re-post step to carry the override onto this new commit,
// whose SHA has no e2e-test/* statuses yet (statuses are keyed by SHA).
core.setOutput('e2e_override', 'true');
return;
}
// Remove first so re-adding always emits pull_request:labeled for Matterwick.
for (const label of runLabels) {
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: label,
});
core.info(`Removed ${label} from PR #${prNumber} before refresh.`);
} catch (err) {
if (err.status !== 404) {
throw err;
}
}
}
core.info(`PR #${prNumber} synchronize — re-adding E2E/Run (Matterwick labeled hook).`);
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber,
labels: ['E2E/Run'],
});
# Post success statuses when E2E is skipped so the PR is not blocked. Reuses
# e2e-override-status (a generic "post success with this description on all 5
# required contexts" tool) instead of hardcoding the context list a second time
# — the whole point of single-sourcing E2E_STATUS_CONTEXTS is that this list
# cannot drift from branch protection's required checks.
post-skip-status:
needs: detect-e2e-impact
if: >-
always()
&& needs.detect-e2e-impact.result == 'success'
&& needs.detect-e2e-impact.outputs.app_impacting != 'true'
&& (github.event.action == 'synchronize' || github.event.action == 'opened')
&& github.event.pull_request.base.ref == 'main'
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
permissions:
contents: read
statuses: write
steps:
# Base, not the PR head. `uses: ./.github/actions/...` loads the composite
# action from the checked-out workspace, so a PR-head checkout would run
# PR-authored action code in a job holding write scopes — and the status
# actions also require() detox/utils/build-tsio-job-config.js from it.
# Nothing here needs PR content: the SHAs come from the event payload.
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Post skip status for all E2E contexts
uses: ./.github/actions/e2e-override-status
with:
commit_sha: ${{ github.event.pull_request.head.sha }}
description: E2E skipped — no app-impacting changes (remove+re-add E2E/Run to force)
target_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Override is SHA-scoped: a commit pushed after the label was applied has no
# e2e-test/* statuses, so a required check would block it. Matterwick is not
# dispatched (run labels were stripped), so re-post the bypass on the new head
# SHA. Idempotent with the label-manager's `labeled` post.
#
# Separate job on purpose. Checking out the PR and running a local action means
# running PR-authored code, so it must not share a runner with the step that
# receives MATTERMOST_BUILD_GH_TOKEN. This job holds no secret and only the two
# permissions the status post needs.
#
# No explicit app_impacting gate needed: this job needs refresh-e2e-run-label,
# and refresh-e2e-run-label's own `if` already requires app_impacting == 'true'.
# GitHub Actions skips a dependent job by default when its `needs` job is
# skipped (no `always()` here), so this is skipped for free on a non-impacting
# push — post-skip-status covers that case instead.
repost-override-status:
needs: refresh-e2e-run-label
if: needs.refresh-e2e-run-label.outputs.e2e_override == 'true'
runs-on: ubuntu-24.04
permissions:
contents: read
statuses: write
steps:
# Base, not the PR head. `uses: ./.github/actions/...` loads the composite
# action from the checked-out workspace, so a PR-head checkout would run
# PR-authored action code in a job holding write scopes — and the status
# actions also require() detox/utils/build-tsio-job-config.js from it.
# Nothing here needs PR content: the SHAs come from the event payload.
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Re-post E2E/Override success on new commit
uses: ./.github/actions/e2e-override-status
with:
commit_sha: ${{ github.event.pull_request.head.sha }}
target_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
add-e2e-run-label:
needs: detect-e2e-impact
if: >-
always()
&& needs.detect-e2e-impact.result == 'success'
&& needs.detect-e2e-impact.outputs.app_impacting == 'true'
&& github.event.action == 'opened'
&& github.event.pull_request.base.ref == 'main'
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
steps:
- name: Add E2E/Run (PAT — emits labeled webhook for Matterwick)
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request.number;
const runLabels = ['E2E/Run', 'E2E/Run-iOS', 'E2E/Run-Android'];
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner, repo, issue_number: prNumber,
});
const labelNames = labels.map((label) => label.name);
if (labelNames.includes('E2E/Override')) {
core.info(`PR #${prNumber} has E2E/Override label — skipping E2E/Run.`);
for (const label of runLabels) {
if (!labelNames.includes(label)) {
continue;
}
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: label,
});
core.info(`Removed stale ${label} from PR #${prNumber} (E2E/Override active).`);
} catch (err) {
if (err.status === 404) {
continue;
}
throw err;
}
}
return;
}
if (labelNames.includes('E2E/Run')) {
core.info(`PR #${prNumber} already has E2E/Run on open — skipping add.`);
return;
}
core.info(`PR #${prNumber} opened — adding E2E/Run via PAT (Matterwick labeled hook).`);
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber,
labels: ['E2E/Run'],
});
e2e-label-manager:
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
statuses: write
# Override cancels the in-flight run (see the cancel step below).
actions: write
steps:
# Base, not the PR head. `uses: ./.github/actions/...` loads the composite
# action from the checked-out workspace, so a PR-head checkout would run
# PR-authored action code in a job holding write scopes — and the status
# actions also require() detox/utils/build-tsio-job-config.js from it.
# Nothing here needs PR content: the SHAs come from the event payload.
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
# ── Honor E2E/Override: strip run labels, then cancel in-flight E2E ─────
- name: Honor E2E/Override
if: >-
github.event.action == 'labeled'
&& github.event.label.name == 'E2E/Override'
&& github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request.number;
const runLabels = ['E2E/Run', 'E2E/Run-iOS', 'E2E/Run-Android'];
for (const label of runLabels) {
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: label,
});
core.info(`Removed ${label} from PR #${prNumber} (E2E/Override applied).`);
} catch (err) {
if (err.status === 404) {
core.info(`${label} was not set on PR #${prNumber}; nothing to remove.`);
continue;
}
throw err;
}
}
# Letting the run finish would not preserve a usable signal: merging the PR
# fires Matterwick's `closed` cleanup (matterwick server/pull_request.go:98 →
# e2e_tests.go:663 → destroyE2EInstances), which deletes the test servers out
# from under a still-running job regardless of whether the run labels were
# removed. The run then reports a red to TSIO that is infra teardown, not a
# test result — and TSIO per-spec history is what engineers read to decide
# whether a spec also fails on main, so recording infra failures there
# degrades flake detection for everyone. Cancel instead.
#
# Runs BEFORE the success post below so our status write lands last. A
# cancelled run's finalize step may still post after us; overrideCommitStatus
# in detox/utils/tsio-report-status.js converts failure → success while the
# label is set, so the check stays green and only the description differs.
- name: Cancel in-flight E2E runs
if: >-
github.event.pull_request.head.repo.full_name == github.repository
&& github.event.action == 'labeled'
&& github.event.label.name == 'E2E/Override'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request.number;
const headBranch = context.payload.pull_request.head.ref;
const ACTIVE_RUN_STATUSES = ['in_progress', 'queued', 'waiting'];
const {data: workflows} = await github.rest.actions.listRepoWorkflows({owner, repo});
const e2eWorkflow = workflows.workflows.find((workflow) => workflow.name === 'E2E');
if (!e2eWorkflow) {
core.info('No E2E workflow found in repository');
return;
}
const runsToCancel = new Map();
for (const status of ACTIVE_RUN_STATUSES) {
const {data} = await github.rest.actions.listWorkflowRuns({
owner, repo, workflow_id: e2eWorkflow.id, status, branch: headBranch,
});
for (const run of data.workflow_runs) {
const matchesPr = !run.pull_requests?.length ||
run.pull_requests.some((pr) => pr.number === prNumber);
if (!matchesPr) {
core.info(`Skipping run ${run.id}: not associated with PR #${prNumber}`);
continue;
}
runsToCancel.set(run.id, run);
}
}
if (runsToCancel.size === 0) {
core.info(`No active E2E runs found for PR #${prNumber} on branch ${headBranch}`);
return;
}
for (const run of runsToCancel.values()) {
core.info(`Cancelling E2E run ${run.id} (${run.status}, sha ${run.head_sha}) — E2E/Override applied`);
try {
await github.rest.actions.cancelWorkflowRun({owner, repo, run_id: run.id});
// The plain cancel above is what stops the expensive build jobs —
// they are `if: !cancelled()`, so cancelling the run ends them.
// What it does NOT stop is anything gated on `always()`, which
// keeps running through a cancellation (tsio-channel-notify, and
// the artifact-upload steps inside the build jobs). Run-level
// force-cancel stops those too.
await github.request('POST /repos/{owner}/{repo}/actions/runs/{run_id}/force-cancel', {
owner, repo, run_id: run.id,
});
} catch (err) {
core.warning(`Cancelling run ${run.id}: ${err.message}`);
}
}
# Override means "merge without waiting on E2E", so every required context
# that is still blocking has to go green on the PR head SHA — whether E2E
# never ran, already failed, or was just cancelled above. Statuses are keyed
# by SHA, so this also clears earlier failures. Contexts that already pass on
# their own are left alone, so the checks list keeps their real results.
# No statuses are posted from the cancel step; this is the single writer.
- name: Publish success on required E2E contexts
if: >-
github.event.pull_request.head.repo.full_name == github.repository
&& github.event.action == 'labeled'
&& github.event.label.name == 'E2E/Override'
uses: ./.github/actions/e2e-override-status
with:
commit_sha: ${{ github.event.pull_request.head.sha }}
target_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Taking the escape hatch away must re-block the PR: without this the
# success posted above would linger on the head SHA forever.
- name: Reset required E2E contexts when E2E/Override is removed
if: >-
github.event.pull_request.head.repo.full_name == github.repository
&& github.event.action == 'unlabeled'
&& github.event.label.name == 'E2E/Override'
uses: ./.github/actions/e2e-override-status
with:
commit_sha: ${{ github.event.pull_request.head.sha }}
state: pending
description: E2E/Override removed — re-run E2E for a real result
# No target_url on purpose. These contexts are E2E results, and at this
# moment there is no E2E run to link to — pointing Details at this
# label-manager run just lands the reader on a labelling job. The E2E
# run posts its own link over this as soon as it starts.
target_url: ''
# E2E/Verified is a narrower waiver than E2E/Override: a maintainer has read
# a real failure and judged it flaky, so only the currently-failing
# e2e-test/* contexts are marked success — a platform that is actually
# green is left alone.
- name: Publish success on flaky-verified E2E contexts
if: >-
github.event.pull_request.head.repo.full_name == github.repository
&& github.event.action == 'labeled'
&& github.event.label.name == 'E2E/Verified'
uses: ./.github/actions/e2e-verified-status
with:
commit_sha: ${{ github.event.pull_request.head.sha }}
mode: apply
actor: ${{ github.event.sender.login }}
target_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Reset exactly what apply touched (matched by the marker in its own
# description), not every required context — a platform that was never
# verified must not be blanked back to pending just because the label
# was removed.
- name: Reset E2E contexts verified flaky when E2E/Verified is removed
if: >-
github.event.pull_request.head.repo.full_name == github.repository
&& github.event.action == 'unlabeled'
&& github.event.label.name == 'E2E/Verified'
uses: ./.github/actions/e2e-verified-status
with:
commit_sha: ${{ github.event.pull_request.head.sha }}
mode: reset
# No target_url — same reason as the E2E/Override reset above.
target_url: ''