diff --git a/.github/workflows/e2e-label-dispatch.yml b/.github/workflows/e2e-label-dispatch.yml deleted file mode 100644 index b58ca306a..000000000 --- a/.github/workflows/e2e-label-dispatch.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: E2E Label Dispatch - -# When a maintainer applies `test:e2e` or `test:e2e-gpu`, dispatch the matching -# self-hosted workflow against the copy-pr-bot mirror branch. Without this, -# the gated workflow only runs on push to `pull-request/`, so a label -# applied after the mirror was created leaves the gate stuck red until someone -# manually re-runs the workflow. -# -# Pushes to `pull-request/` (whether from an automatic copy-pr-bot sync or -# a maintainer-typed `/ok to test`) already re-trigger the gated workflow on -# their own - only the label-application case needs this dispatcher. -# -# Uses `pull_request_target` so forked PRs get a write-capable token. The job -# never checks out PR code; it only calls the GitHub API. - -on: - pull_request_target: - types: [labeled] - -permissions: {} - -jobs: - dispatch: - name: Dispatch E2E workflow for labeled PR - if: github.event.label.name == 'test:e2e' || github.event.label.name == 'test:e2e-gpu' - runs-on: ubuntu-latest - permissions: - actions: write - pull-requests: write - steps: - - name: Dispatch workflow against the copy-pr-bot mirror - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - LABEL_NAME: ${{ github.event.label.name }} - shell: bash - run: | - set -euo pipefail - - case "$LABEL_NAME" in - test:e2e) workflow=branch-e2e.yml ;; - test:e2e-gpu) workflow=test-gpu.yml ;; - *) echo "Unrecognized label $LABEL_NAME"; exit 1 ;; - esac - - mirror_ref="pull-request/$PR_NUMBER" - mirror_sha=$(gh api "repos/$GH_REPO/branches/$mirror_ref" --jq '.commit.sha' 2>/dev/null || echo "") - short_pr=${PR_HEAD_SHA:0:7} - - if [ -z "$mirror_sha" ]; then - gh pr comment "$PR_NUMBER" --body "Label \`$LABEL_NAME\` applied, but copy-pr-bot has not mirrored this PR yet. A maintainer needs to comment \`/ok to test $PR_HEAD_SHA\` to start the mirror; re-apply the label once \`$mirror_ref\` exists." - exit 0 - fi - - if [ "$mirror_sha" != "$PR_HEAD_SHA" ]; then - short_mirror=${mirror_sha:0:7} - gh pr comment "$PR_NUMBER" --body "Label \`$LABEL_NAME\` applied, but \`$mirror_ref\` is at \`$short_mirror\` while the PR head is \`$short_pr\`. Comment \`/ok to test $PR_HEAD_SHA\` to refresh the mirror, then re-apply the label." - exit 0 - fi - - echo "Dispatching $workflow against $mirror_ref ($short_pr) for label $LABEL_NAME." - gh workflow run "$workflow" --ref "$mirror_ref" - gh pr comment "$PR_NUMBER" --body "Dispatched \`$workflow\` against \`$mirror_ref\` at \`$short_pr\` (label \`$LABEL_NAME\`). Results will post as checks on this PR." diff --git a/.github/workflows/e2e-label-help.yml b/.github/workflows/e2e-label-help.yml new file mode 100644 index 000000000..517bd9b30 --- /dev/null +++ b/.github/workflows/e2e-label-help.yml @@ -0,0 +1,64 @@ +name: E2E Label Help + +# When a `test:e2e` / `test:e2e-gpu` label is applied, post a PR comment +# telling the maintainer the next manual step. We don't dispatch the workflow +# ourselves: a workflow_dispatch-triggered run does not surface in the PR's +# Checks tab, so we'd lose in-progress visibility. Instead we point the +# maintainer at either the existing run (re-run from the UI) or the +# `/ok to test ` command needed to refresh the mirror. +# +# Uses `pull_request_target` so forked PRs get a token capable of posting +# comments. The job never checks out PR code; it only calls the GitHub API. + +on: + pull_request_target: + types: [labeled] + +permissions: {} + +jobs: + hint: + name: Post next-step hint for E2E label + if: github.event.label.name == 'test:e2e' || github.event.label.name == 'test:e2e-gpu' + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Post comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + LABEL_NAME: ${{ github.event.label.name }} + shell: bash + run: | + set -euo pipefail + + case "$LABEL_NAME" in + test:e2e) workflow_file=branch-e2e.yml; workflow_name="Branch E2E Checks" ;; + test:e2e-gpu) workflow_file=test-gpu.yml; workflow_name="GPU Test" ;; + *) echo "Unrecognized label $LABEL_NAME"; exit 1 ;; + esac + + mirror_ref="pull-request/$PR_NUMBER" + mirror_sha=$(gh api "repos/$GH_REPO/branches/$mirror_ref" --jq '.commit.sha' 2>/dev/null || echo "") + short_pr=${PR_HEAD_SHA:0:7} + + if [ -z "$mirror_sha" ]; then + body="Label \`$LABEL_NAME\` applied, but \`$mirror_ref\` does not exist yet. A maintainer needs to comment \`/ok to test $PR_HEAD_SHA\` to mirror this PR. Once the mirror exists, re-apply the label or re-run [$workflow_name](https://github.com/$GH_REPO/actions/workflows/$workflow_file) from the Actions tab." + elif [ "$mirror_sha" != "$PR_HEAD_SHA" ]; then + short_mirror=${mirror_sha:0:7} + body="Label \`$LABEL_NAME\` applied, but \`$mirror_ref\` is at \`$short_mirror\` while the PR head is \`$short_pr\`. A maintainer needs to comment \`/ok to test $PR_HEAD_SHA\` to refresh the mirror. Once the mirror catches up, re-run [$workflow_name](https://github.com/$GH_REPO/actions/workflows/$workflow_file) from the Actions tab." + else + run_id=$(gh api "repos/$GH_REPO/actions/workflows/$workflow_file/runs?head_sha=$PR_HEAD_SHA&event=push" \ + --jq '.workflow_runs | sort_by(.created_at) | reverse | .[0].id // empty') + if [ -n "$run_id" ]; then + run_link="[the existing run](https://github.com/$GH_REPO/actions/runs/$run_id)" + else + run_link="[$workflow_name](https://github.com/$GH_REPO/actions/workflows/$workflow_file)" + fi + body="Label \`$LABEL_NAME\` applied for \`$short_pr\`. Open $run_link and click **Re-run all jobs** to execute with the label set. The \`E2E Gate\` check on this PR will flip green automatically once the run finishes." + fi + + gh pr comment "$PR_NUMBER" --body "$body" diff --git a/CI.md b/CI.md index b849919bd..d39ecdb45 100644 --- a/CI.md +++ b/CI.md @@ -63,10 +63,11 @@ Prerequisites: Flow: 1. Open the PR. copy-pr-bot mirrors it to `pull-request/` automatically. -2. A maintainer applies `test:e2e` and/or `test:e2e-gpu`. -3. `E2E Label Dispatch` detects the label and triggers the matching workflow against the mirror. -4. Results post as checks on your PR head SHA. -5. New commits push to the mirror automatically; gated workflows re-run on their own. No re-labeling needed. +2. The first push of `pull-request/` runs `Branch E2E Checks`, but it skips the build/E2E jobs because no label is set yet. The PR's `E2E Gate` check stays neutral (no label, no requirement). +3. A maintainer applies `test:e2e` and/or `test:e2e-gpu`. `E2E Label Help` posts a comment with a link to the existing `Branch E2E Checks` run. +4. The maintainer opens that link and clicks **Re-run all jobs**. This time `pr_metadata` sees the label and the build/E2E jobs run. +5. When the run finishes, the `E2E Gate` check on the PR flips to green automatically. +6. New commits push to the mirror automatically and re-trigger `Branch E2E Checks`. Because the label is still set, those runs execute the build/E2E jobs without manual re-run. ### Forked PR @@ -79,11 +80,9 @@ Flow: 1. Open the PR. The vouch check confirms you are vouched (otherwise the PR is auto-closed). 2. copy-pr-bot does not mirror forks automatically. A maintainer reviews the diff and comments `/ok to test ` with your latest commit SHA. -3. After `/ok to test`, copy-pr-bot mirrors to `pull-request/`. -4. A maintainer applies `test:e2e` / `test:e2e-gpu`. The dispatcher runs the matching workflow against the mirror. -5. Results post as checks on your PR. +3. After `/ok to test`, copy-pr-bot mirrors to `pull-request/`. From here the flow is identical to internal PRs: maintainer applies the label, follows the comment from `E2E Label Help`, and re-runs the workflow. -Important: every new commit you push requires another `/ok to test ` from a maintainer before E2E will run on it. If a label is applied while the mirror is stale, `E2E Label Dispatch` will post a comment explaining what's needed. +Important: every new commit you push requires another `/ok to test ` from a maintainer before E2E will run on it. If a label is applied while the mirror is stale, `E2E Label Help` will post a comment explaining what's needed. ## copy-pr-bot @@ -108,4 +107,4 @@ The bot's full administrator documentation is internal to NVIDIA. The only comma | `.github/actions/pr-gate/action.yml` | Composite action that resolves PR metadata and verifies the required label is set. | | `.github/workflows/e2e-gate.yml` | Posts the required `E2E Gate` check on the PR. Re-evaluates after the gated workflow completes. | | `.github/workflows/e2e-gate-check.yml` | Reusable gate logic shared by E2E and GPU E2E. | -| `.github/workflows/e2e-label-dispatch.yml` | Triggers gated workflows when a `test:e2e*` label is applied. Posts a comment if the mirror is missing or stale. | +| `.github/workflows/e2e-label-help.yml` | When a `test:e2e*` label is applied, posts a PR comment telling the maintainer the next manual step (re-run an existing workflow run, or `/ok to test ` to refresh the mirror). | diff --git a/architecture/ci-e2e.md b/architecture/ci-e2e.md index 3015df971..3b4332463 100644 --- a/architecture/ci-e2e.md +++ b/architecture/ci-e2e.md @@ -22,7 +22,7 @@ These three goals do not compose cleanly: the safety goal forces `push: pull-req | `.github/actions/pr-gate/action.yml` | (composite) | Resolves PR metadata for a `pull-request/` push and decides whether the run should proceed. Used by the two workflows above. | | `.github/workflows/e2e-gate.yml` | `pull_request` + `workflow_run` | Posts the required `E2E Gate` check on the PR. Re-evaluates after the gated workflow completes. | | `.github/workflows/e2e-gate-check.yml` | `workflow_call` | Reusable gate logic shared by E2E and GPU E2E. | -| `.github/workflows/e2e-label-dispatch.yml` | `pull_request_target: [labeled]` | Dispatches the gated workflow when a `test:e2e*` label is applied after the mirror already exists. | +| `.github/workflows/e2e-label-help.yml` | `pull_request_target: [labeled]` | Posts a PR comment when a `test:e2e*` label is applied, telling the maintainer the next manual step (re-run an existing run, or `/ok to test ` to refresh the mirror). Does *not* dispatch the workflow itself - see "Why we don't auto-dispatch" below. | | `.github/workflows/e2e-test.yml`, `e2e-gpu-test.yaml`, `docker-build.yml` | `workflow_call` | Reusable worker workflows. Unchanged by this design - called from the gated workflows and from release workflows. | ## Trigger taxonomy @@ -33,13 +33,13 @@ Five GitHub Actions trigger types appear in this flow. Each one was chosen for a |---|---|---|---| | `push: pull-request/[0-9]+` | The pushed commit (mirror branch) | Repo-default | Only fires for branches copy-pr-bot created. Decouples test execution from PR author actions: the author cannot create a `pull-request/` branch themselves. | | `pull_request` | The PR head SHA, but actions checkout the *base* branch's workflow files | Read-only for forks | Lets us post a status check on the PR's head SHA (so branch protection sees it). Used by the `E2E Gate` evaluation jobs. | -| `pull_request_target` | Base branch | Write-capable, even for forks | Needed for the label dispatcher: a forked PR's `GITHUB_TOKEN` is read-only on `pull_request`, so it cannot dispatch workflows or post comments. The dispatcher never checks out PR code, so the `pull_request_target` foot-gun does not apply. | -| `workflow_run` | Default branch | Repo-default | Fires when the gated workflow finishes. Lets us run a re-evaluation step in a trusted (default-branch) context. | -| `workflow_dispatch` | Caller's ref | Repo-default | Maintainer escape hatch and the mechanism the label dispatcher uses to call the gated workflow (`gh workflow run --ref pull-request/`). | +| `pull_request_target` | Base branch | Write-capable, even for forks | Needed for `e2e-label-help.yml` to post a comment on a forked PR. The workflow never checks out PR code, so the standard `pull_request_target` foot-gun does not apply. | +| `workflow_run` | Default branch | Repo-default | Fires when the gated workflow finishes. Lets us run a gate re-evaluation step in a trusted (default-branch) context. | +| `workflow_dispatch` | Caller's ref | Repo-default | Maintainer-only manual re-run (clicking "Re-run all jobs" in the Actions UI). We deliberately do not call this from another workflow - see "Why we don't auto-dispatch" below. | -The non-obvious move here is that the same logical "did E2E pass for this PR" check has to be posted from at least two of these trigger contexts: a `pull_request`-triggered run (which can attach a check to the PR head SHA) and a `workflow_run`-triggered run (which knows the gated workflow finished but can only attach checks to `main`). The flow stitches them together by re-running the original `pull_request`-triggered run after the gated workflow completes. +The non-obvious move here is that the same logical "did E2E pass for this PR" check has to be posted from two of these trigger contexts: a `pull_request`-triggered run (which can attach a check to the PR head SHA) and a `workflow_run`-triggered run (which knows the gated workflow finished but can only attach checks to `main`). The flow stitches them together by re-running the original `pull_request`-triggered run after the gated workflow completes. -## Happy-path flow (trusted PR with label set up-front) +## Happy-path flow (trusted PR, label applied after mirror) ```mermaid sequenceDiagram @@ -49,7 +49,7 @@ sequenceDiagram participant Bot as copy-pr-bot participant BranchE2E as Branch E2E Checks
(self-hosted) participant Gate as E2E Gate
(github-hosted) - participant Dispatcher as E2E Label Dispatch
(github-hosted) + participant Help as E2E Label Help
(github-hosted) participant Maintainer Author->>GH: Open PR (signed commits) @@ -65,9 +65,10 @@ sequenceDiagram Maintainer->>GH: apply test:e2e label GH->>Gate: pull_request labeled Gate->>Gate: label set,
upstream only ran metadata
→ FAIL (red) - GH->>Dispatcher: pull_request_target labeled - Dispatcher->>GH: gh workflow run branch-e2e.yml
--ref pull-request/N - GH->>BranchE2E: workflow_dispatch + GH->>Help: pull_request_target labeled + Help->>GH: comment on PR with link
to existing Branch E2E Checks run + Maintainer->>GH: open the linked run, click "Re-run all jobs" + GH->>BranchE2E: re-run (push event replayed) BranchE2E->>BranchE2E: pr_metadata: should_run = true
(label set, SHA matches) BranchE2E->>BranchE2E: build + e2e jobs run @@ -78,7 +79,7 @@ sequenceDiagram Gate->>Gate: label set,
upstream success + non-gate jobs ran
→ PASS (green) ``` -The key moment is step 11: the dispatcher closes the loop that the `push` trigger doesn't. Without it, applying the label leaves the gate stuck red until a maintainer manually re-runs the gated workflow. +The label-help workflow is intentionally a comment-only nudge: it never dispatches the workflow itself, so the maintainer's re-run goes through the same `push`-event run-id that originally fired on the mirror. This preserves in-progress visibility on the PR's Checks tab. ## Forked PR flow @@ -97,7 +98,7 @@ sequenceDiagram Bot->>Bot: not trusted, wait Maintainer->>GH: comment "/ok to test " Bot->>GH: push pull-request/N - Note over Bot,GH: From here, identical to the trusted flow:
label → dispatcher → gated workflow → rerun gate + Note over Bot,GH: From here, identical to the trusted flow:
label → help comment → maintainer re-runs → gate flips green Author->>GH: push new commit Bot->>Bot: still untrusted, wait again Maintainer->>GH: comment "/ok to test " @@ -115,19 +116,27 @@ The gated workflows always start with a `pr_metadata` job. When the label is mis ### Why `workflow_run` is needed for the gate flip -Once the label dispatcher kicks off `branch-e2e.yml`, the workflow runs and finishes - but the `pull_request`-triggered gate check posted earlier still says "fail". `workflow_run` is the only event that fires when an arbitrary other workflow completes, and it's how we know to re-evaluate the gate. But `workflow_run` runs in the *default branch context*, so a check posted from there lands on `main` instead of the PR. Workaround: instead of posting a new check, look up the most recent `pull_request`-triggered gate run for the same head SHA and call `POST /actions/runs//rerun`. The re-run replays the original `pull_request` event, so the new check posts against the PR's head SHA and branch protection picks it up. +Once the gated workflow runs and finishes, the `pull_request`-triggered gate check posted earlier still says "fail". `workflow_run` is the only event that fires when an arbitrary other workflow completes, and it's how we know to re-evaluate the gate. But `workflow_run` runs in the *default branch context*, so a check posted from there lands on `main` instead of the PR. Workaround: instead of posting a new check, look up the most recent `pull_request`-triggered gate run for the same head SHA and call `POST /actions/runs//rerun`. The re-run replays the original `pull_request` event, so the new check posts against the PR's head SHA and branch protection picks it up. -### Why `pull_request_target` for the label dispatcher +### Why `pull_request_target` for the label-help workflow -A `pull_request` workflow on a forked PR receives a read-only `GITHUB_TOKEN`. That's intentional: it prevents PR-supplied workflow code from escalating. But our dispatcher doesn't *run* PR code - it never checks out the PR head, only the workflow file from `main`. It needs `actions: write` to dispatch the gated workflow and `pull-requests: write` to post a status comment. `pull_request_target` provides a write-capable token while still loading the workflow definition from `main`. The standard `pull_request_target` warning ("don't check out PR code with this token") doesn't apply here because we don't check out anything. +A `pull_request` workflow on a forked PR receives a read-only `GITHUB_TOKEN`. That's intentional: it prevents PR-supplied workflow code from escalating. But the help workflow doesn't *run* PR code - it never checks out the PR head, only the workflow file from `main`. It needs `pull-requests: write` to post a comment. `pull_request_target` provides a write-capable token while still loading the workflow definition from `main`. The standard `pull_request_target` warning ("don't check out PR code with this token") doesn't apply because we don't check out anything. -### Why labels and not comment commands +### Why we don't auto-dispatch the gated workflow -Labels persist as PR metadata and survive re-runs and force-pushes. Comment-based commands like `/ok to test` don't survive the same way: a comment from yesterday doesn't enable today's commit. Branch protection rules can require a check be present; they cannot require a comment. The label is the merge gate's primary signal because it is the only thing GitHub's branch protection knows how to look at. +An earlier iteration of this design auto-dispatched the gated workflow via `gh workflow run --ref pull-request/` from a `pull_request_target: [labeled]` workflow. It worked, but produced a worse UX: `workflow_dispatch`-triggered runs do not appear in the PR's Checks tab. The check-runs are technically attached to the PR head SHA (visible via `gh api commits//check-runs`), but the PR UI filters them out because the run isn't associated with a PR-context event. The maintainer would see "Dispatched" comment, then no progress on the PR until the gate eventually flipped from red to green many minutes later. + +We considered alternatives: -### Why a separate `E2E Label Dispatch` workflow instead of dispatching from the gate +- **Push an empty marker commit to `pull-request/` to fire a fresh `push` event.** Changes the SHA, breaks the gate's head-SHA equivalence, and writes to a branch copy-pr-bot owns. Architecturally bad. +- **Re-trigger copy-pr-bot programmatically.** copy-pr-bot only listens for `pull_request.*` and `issue_comment.created` events ([source](https://github.com/NVIDIA/gha-runners-apps/blob/main/packages/copy-pr-bot/src/app.ts)). Even commenting `/ok to test ` is a no-op when the mirror is already at that SHA - the bot calls `git.updateRef` with the same SHA and GitHub fires no new push event. There is no way to make copy-pr-bot re-fire a push without an actual SHA change. +- **Have the dispatcher post mirror Check Runs against the PR head SHA via the Checks API.** Possible, but adds a polling/webhook loop to keep the mirror checks in sync with the actual run. Not worth the complexity for a flow a maintainer goes through manually anyway. -The gate runs on `pull_request`. If we tried to dispatch the gated workflow from the gate, the dispatch call would fail on forked PRs because the gate's token is read-only. Splitting the dispatcher into its own `pull_request_target` workflow gives it the write-capable token without giving it to the gate evaluation logic. +The current design takes the pragmatic path: when a label is applied, the help workflow posts a comment with a deep link to the existing `Branch E2E Checks` run on the mirror. The maintainer clicks **Re-run all jobs**. That re-run replays the original `push` event, so its check-runs surface on the PR's Checks tab in real time. The cost is one human click per label application, in exchange for live progress visibility. + +### Why labels and not comment commands + +Labels persist as PR metadata and survive re-runs and force-pushes. Comment-based commands like `/ok to test` don't survive the same way: a comment from yesterday doesn't enable today's commit. Branch protection rules can require a check be present; they cannot require a comment. The label is the merge gate's primary signal because it is the only thing GitHub's branch protection knows how to look at. ## Permission posture @@ -141,16 +150,11 @@ Every workflow declares `permissions: {}` at the top. Per-job grants are the min | `e2e-gate.yml` | `e2e`, `gpu` (`workflow_call`) | inherits via the called workflow | | | `rerun-on-completion` | `actions: write` | | `e2e-gate-check.yml` | `check` | `contents: read`, `pull-requests: read`, `actions: read` | -| `e2e-label-dispatch.yml` | `dispatch` | `actions: write`, `pull-requests: write` | +| `e2e-label-help.yml` | `hint` | `pull-requests: write` | The reusable worker workflows (`e2e-test.yml`, `e2e-gpu-test.yaml`, `docker-build.yml`) declare their own internal permissions; the calling job grants are an upper bound for them. -Two workflows hold "interesting" tokens: - -- `rerun-on-completion` in `e2e-gate.yml` has `actions: write`. It calls one specific endpoint - `POST /actions/runs//rerun` for an `e2e-gate.yml` run on the same head SHA - and never executes PR code. -- `dispatch` in `e2e-label-dispatch.yml` has `actions: write` + `pull-requests: write`. It calls `gh workflow run` against `branch-e2e.yml` or `test-gpu.yml` (only) and posts a comment on the PR. It never executes PR code. - -Both are small, github-hosted workflows whose source lives only on `main`. +Only one workflow holds an "interesting" token: `rerun-on-completion` in `e2e-gate.yml` has `actions: write`. It calls one specific endpoint - `POST /actions/runs//rerun` for an `e2e-gate.yml` run on the same head SHA - and never executes PR code. The label-help workflow holds only `pull-requests: write` for posting the comment, also without checking out PR code. ## Release flow @@ -162,11 +166,11 @@ Permissions on the release workflows are not yet scoped per-job. Tracked separat | Case | What happens | |---|---| -| Label applied before copy-pr-bot mirrors the PR | Dispatcher detects no `pull-request/` branch and posts a comment telling the maintainer to wait or run `/ok to test `. | -| Label applied while mirror is stale (new commit pending `/ok to test`) | Dispatcher detects mirror SHA != PR head SHA and posts the corresponding comment with the SHA the maintainer needs to vet. | -| Label removed | No dispatcher reaction. The next PR event (push, label, etc.) re-evaluates the gate, which now sees no label and passes as a no-op. | -| Author force-pushes after label set | copy-pr-bot re-mirrors the new SHA → gated workflow fires on `push` → finishes → `workflow_run` fires the gate re-run → new green check on the new SHA. No re-labeling needed. | -| Maintainer re-runs the gated workflow manually from the Actions UI | Same as above without the force-push. | +| Label applied before copy-pr-bot mirrors the PR | Help workflow detects no `pull-request/` branch and posts a comment telling the maintainer to wait or run `/ok to test `. | +| Label applied while mirror is stale (new commit pending `/ok to test`) | Help workflow detects mirror SHA != PR head SHA and posts the corresponding comment with the SHA the maintainer needs to vet. | +| Label removed | No reaction. The next PR event (push, label, etc.) re-evaluates the gate, which now sees no label and passes as a no-op. | +| Author force-pushes after label set | copy-pr-bot re-mirrors the new SHA → gated workflow fires on `push` → because the label is still on the PR, `pr_metadata` runs the build/E2E jobs without manual re-run → `workflow_run` fires the gate re-run → new green check on the new SHA. | +| Maintainer re-runs the gated workflow manually from the Actions UI | Same as above without the force-push. This is the path the help workflow points the maintainer at. | | Gate's first evaluation fails (label set, upstream not yet started) | Email-on-failure noise. The check eventually flips to success once upstream finishes and `workflow_run` re-runs the gate. Tracked as a known rough edge; possible fix is posting `neutral` until the upstream completes. | ## References