Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 23 additions & 54 deletions .agents/skills/nemoclaw-maintainer-day/MERGE-GATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The checker requires these status-rollup entries:
A first-time fork contributor might need **Approve and run** before `pull_request` checks appear.
The E2E controller records the PR SHA and base SHA without running fork code.
Do not waive a missing, neutral, or skipped E2E gate.
Do not run fork code with privileged credentials to create a result.
Fork code can receive E2E credentials only through the protected exact-revision approval below.

### GitHub Actions evidence

Expand All @@ -74,7 +74,11 @@ Required PR workflows must identify the PR number, PR SHA, and base SHA.
- A closed event must use `E2E / PR Gate (not applicable)` for its skipped observer.
It must not publish the required check name.
- GitHub usually associates the run with the PR.
If that association is empty, require the controller path, branch, repository, and run time to match the coordination check.
For a fork run with an empty association, require the Actions event, workflow path, fork repository, branch, and PR SHA to match the current PR.
The PR and installer workflows must also name the exact PR, PR SHA, and base SHA.
The E2E controller must still enclose the trusted coordination check.
- If GitHub omits `headRepository.nameWithOwner`, derive it only from the returned repository name and repository-owner login.
Fail closed when those fields are missing, malformed, or contradictory.
- Treat an all-skipped `gate false` run from an older workflow version as non-evidence.
- Fail closed when identity, state, or timing evidence is missing, malformed, stale, contradictory, or changed.

Expand Down Expand Up @@ -133,73 +137,38 @@ Malformed or unsafe evidence is a terminal controller error.
Schema mismatches, identity mismatches, and traversal-limit errors are also terminal.
The coordination check, required job, and controller must fail closed.

### Approve an E2E skip for a fork PR
### Authorize E2E for a fork PR

Use the protected-environment path when the coordination check reports `Maintainer approval required to skip credentialed E2E`.
Use the protected-environment path when coordination reports `E2E reviewer authorization required to run fork E2E`.

1. Follow the `E2E / PR Gate Controller run <id>` link in the coordination summary.
2. Select **Review deployments**.
3. Select `approve-credentialed-e2e-skip-for-fork-pr`.
4. Add a comment when useful, and approve.
2. Verify the fork repository, PR SHA, base SHA, selected jobs and targets, and risk-plan artifact.
3. Select **Review deployments**.
4. Select `approve-credentialed-e2e-for-fork-pr`.
5. Add a comment when useful, and approve.

This approval records that credential-bearing jobs and targets did not run.
It does not authorize fork code to use repository secrets.
The waiting job has `deployment: false`, no secrets, and no PR-controlled execution.
This approval authorizes the exact fork revision to run the selected work with E2E credentials.
Before approval, no selected credential-bearing work runs.
The trusted workflow definition comes from `main`; each PR-code checkout uses the reviewed fork repository and exact PR SHA.

The controller reads the approval history and requires one approval for that environment.
The reviewer must still have `maintain` or `admin` access.
The controller also checks the PR number, PR SHA, base SHA, plan, failed check, compatible `main`, and PR state.
The environment's required reviewers are the authorization allowlist.
The controller also checks the PR number, head repository, PR SHA, base SHA, plan, pending coordination check, compatible `main`, and PR state.
Immediately before dispatch, it confirms that the PR is open and that the PR SHA, base SHA, and coordination identity still match.

An accepted approval completes the coordination check with this result:
`Credentialed E2E skipped for fork PR — approved by @<maintainer>`.
The summary starts with `Outcome: APPROVED SKIP — credentialed E2E did not run.`
Treat this result as an audited skip, not as E2E evidence.
Approval returns coordination to `Running <count> E2E check(s)`.
The gate passes only after the selected jobs and targets return verified passing evidence.
Failed, missing, skipped, pending, or mismatched evidence keeps the gate from passing.

Configure the environment before rollout.
Require reviewers with `maintain` or `admin` access.
Require the intended E2E reviewers.
Do not add secrets, variables, or a protection app. Disable administrator bypass when possible.

If **Review deployments** is absent, the environment might be missing, unprotected, or no longer waiting.
Configure it, push a change, and run PR CI again. You can also use the manual fallback below.
Configure it, push a change, and run PR CI again.
Do not rerun the waiting workflow. The controller accepts an environment approval only on the first attempt.
Per-PR concurrency cancels a waiting approval when another SHA reaches the gate.

### Use the manual fork-skip fallback

Use this fallback only after the gate reports `Maintainer approval required to skip credentialed E2E`.
Review the fork change and non-secret PR CI first. Prefer the protected-environment path.

The controller rejects these states:

- The PR is closed.
- The PR SHA or base SHA changed.
- The PR is internal.
- The E2E plan is empty.
- The gate did not fail.
- The controller commit is not `main` or a validated ancestor of `main`.
- The reviewer does not have maintainer access.

A permitted `main` advance must keep the controller commit as its merge base.
It must contain fewer than 300 listed changed files and no `e2e-control-plane` changes.
Other advances fail closed.
Immediately before it writes success, the controller confirms that the PR is open and that the PR SHA, base SHA, and coordination identity still match.
It fails closed if any value changed or does not match.
Its result records the reviewer, optional comment, approval-run URL, plan, and work that did not run.

To use the fallback, select **Run workflow** on `main` and then select `approve-fork-e2e-skip`.
Provide these inputs:

- The fork PR number.
- The 40-character PR SHA as `expected_head_sha`.
- The 40-character base SHA as `expected_base_sha`.
- A reason of 10 to 500 characters.

Read both SHAs before dispatch.
You can give an `evidence_url` that matches `https://github.com/NVIDIA/NemoClaw/actions/runs/<run-id>`.
Leave it blank when no supporting run exists.
The workflow rejects PR, issue, comment, job, and external URLs.
It validates the repository and run-ID format. It does not inspect that run's SHA, jobs, targets, or result.

### Authorize E2E control-plane changes

The `e2e-control-plane` path group includes these areas:
Expand Down
101 changes: 82 additions & 19 deletions .agents/skills/nemoclaw-maintainer-day/scripts/check-gates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ const INSTALLER_HASH_RUN_TITLE =
/^Installer Hash PR #([1-9][0-9]*) head ([a-f0-9]{40}) base ([a-f0-9]{40}) gate (true|false)$/u;
const E2E_GATE_RUN_TITLE =
/^E2E Gate PR #([1-9][0-9]*) head ([a-f0-9]{40}) base ([a-f0-9]{40}) gate (true|false)$/u;
const REPOSITORY_NAME_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
const REQUIRED_CHECK_WORKFLOW_PATHS = new Map([
["checks", ".github/workflows/pr.yaml"],
["changes", ".github/workflows/pr.yaml"],
Expand Down Expand Up @@ -945,39 +946,60 @@ function currentCheckRollup(
check.detailsUrl?.match(/\/runs\/(\d+)(?:[/?#]|$)/u)?.[1] ===
String(e2eCoordinationEvidence.trustedLegacyCheckId);

const associationLessHeadBinding = (
metadata: ActionRunMetadata,
): "current" | "other" | "unknown" => {
if (
metadata.hasPullRequests !== false ||
(metadata.event !== "pull_request" && metadata.event !== "pull_request_target")
) {
return "unknown";
}
if (
metadata.headShaMatches === false ||
metadata.headRefNameMatches === false ||
metadata.headRepositoryMatches === false
) {
return "other";
}
return metadata.headShaMatches === true &&
metadata.headRefNameMatches === true &&
metadata.headRepositoryMatches === true
? "current"
: "unknown";
};

function runIdentityEvidence(
runId: string,
requiresExactDiff: boolean,
): "current" | "other" | "unknown" {
const metadata = actionRunMetadata(runId);
if (!metadata?.event || !metadata.path) return "unknown";
const headBinding = associationLessHeadBinding(metadata);
if (
metadata.event === "pull_request" &&
metadata.path === ".github/workflows/installer-hash-check.yaml"
(metadata.path === ".github/workflows/installer-hash-check.yaml" ||
metadata.path === ".github/workflows/pr.yaml")
) {
if (
metadata.immutablePrDiff === false ||
metadata.exactDiff === false ||
metadata.headShaMatches === false
metadata.headShaMatches === false ||
headBinding === "other"
) {
return "other";
}
if (
metadata.immutablePrDiff === true &&
metadata.headShaMatches === true &&
(metadata.exactDiff === true || metadata.hasPullRequests === false)
(metadata.exactDiff === true || headBinding === "current")
) {
return "current";
}
return "unknown";
}
if (metadata.exactDiff === true) {
if (metadata.headShaMatches === true) {
if (metadata.event === "pull_request" && metadata.path === ".github/workflows/pr.yaml") {
if (metadata.immutablePrDiff === true) return "current";
if (metadata.immutablePrDiff === false) return "other";
return "unknown";
}
return "current";
}
if (metadata.headShaMatches === false) return "other";
Expand All @@ -989,6 +1011,13 @@ function currentCheckRollup(
if (e2eHeadBinding === "current") {
return e2eCoordinationIsEnclosed(metadata) ? "current" : "unknown";
}
if (
exactDiff.headRepository !== repo &&
metadata.path !== ".github/workflows/pr-e2e-gate.yaml" &&
headBinding !== "unknown"
) {
return headBinding;
}
if (
!requiresExactDiff &&
metadata.hasPullRequests === false &&
Expand Down Expand Up @@ -1654,6 +1683,37 @@ interface PrRevisionSnapshot {
headRepository: string;
}

function parseHeadRepository(headRepository: unknown, headRepositoryOwner: unknown): string | null {
if (
typeof headRepository !== "object" ||
headRepository === null ||
Array.isArray(headRepository)
) {
return null;
}
const repository = headRepository as Record<string, unknown>;
const direct =
typeof repository.nameWithOwner === "string" &&
REPOSITORY_NAME_PATTERN.test(repository.nameWithOwner)
? repository.nameWithOwner
: null;
let derived: string | null = null;
if (
typeof repository.name === "string" &&
/^[A-Za-z0-9_.-]+$/u.test(repository.name) &&
typeof headRepositoryOwner === "object" &&
headRepositoryOwner !== null &&
!Array.isArray(headRepositoryOwner)
) {
const login = (headRepositoryOwner as Record<string, unknown>).login;
if (typeof login === "string" && /^[A-Za-z0-9_.-]+$/u.test(login)) {
derived = `${login}/${repository.name}`;
}
}
if (direct && derived && direct !== derived) return null;
return direct ?? derived;
}

function fetchPrRevisionSnapshot(repo: string, number: number): PrRevisionSnapshot | null {
const value = ghJson([
"pr",
Expand All @@ -1662,11 +1722,11 @@ function fetchPrRevisionSnapshot(repo: string, number: number): PrRevisionSnapsh
"--repo",
repo,
"--json",
"title,body,state,isDraft,mergeable,mergeStateStatus,headRefOid,baseRefOid,headRefName,baseRefName,headRepository",
"title,body,state,isDraft,mergeable,mergeStateStatus,headRefOid,baseRefOid,headRefName,baseRefName,headRepository,headRepositoryOwner",
]);
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
const record = value as Record<string, unknown>;
const headRepository = record.headRepository;
const headRepository = parseHeadRepository(record.headRepository, record.headRepositoryOwner);
if (
typeof record.title !== "string" ||
typeof record.body !== "string" ||
Expand All @@ -1678,10 +1738,7 @@ function fetchPrRevisionSnapshot(repo: string, number: number): PrRevisionSnapsh
typeof record.baseRefOid !== "string" ||
typeof record.headRefName !== "string" ||
typeof record.baseRefName !== "string" ||
typeof headRepository !== "object" ||
headRepository === null ||
Array.isArray(headRepository) ||
typeof (headRepository as Record<string, unknown>).nameWithOwner !== "string"
!headRepository
) {
return null;
}
Expand All @@ -1696,7 +1753,7 @@ function fetchPrRevisionSnapshot(repo: string, number: number): PrRevisionSnapsh
baseRefOid: record.baseRefOid,
headRefName: record.headRefName,
baseRefName: record.baseRefName,
headRepository: (headRepository as Record<string, unknown>).nameWithOwner as string,
headRepository,
};
}

Expand Down Expand Up @@ -1773,7 +1830,7 @@ function main(): void {
"--repo",
repo,
"--json",
"number,title,url,body,files,statusCheckRollup,state,isDraft,mergeable,mergeStateStatus,headRefOid,baseRefOid,headRefName,baseRefName,headRepository,author",
"number,title,url,body,files,statusCheckRollup,state,isDraft,mergeable,mergeStateStatus,headRefOid,baseRefOid,headRefName,baseRefName,headRepository,headRepositoryOwner,author",
]) as {
number: number;
title: string;
Expand All @@ -1789,21 +1846,27 @@ function main(): void {
baseRefOid: string;
headRefName: string;
baseRefName: string;
headRepository: { nameWithOwner: string };
headRepository: { name: string; nameWithOwner: string };
headRepositoryOwner: { login: string } | null;
author: PrIdentity | null;
} | null;

if (!prData) {
console.error(`Failed to fetch PR #${prNumber} from ${repo}`);
process.exit(1);
}
const headRepository = parseHeadRepository(prData.headRepository, prData.headRepositoryOwner);
if (!headRepository) {
console.error(`Failed to resolve PR #${prNumber} head repository from ${repo}`);
process.exit(1);
}

const ci = checkCi(prData.statusCheckRollup, repo, {
number: prNumber,
headSha: prData.headRefOid,
baseSha: prData.baseRefOid,
headRefName: prData.headRefName,
headRepository: prData.headRepository.nameWithOwner,
headRepository,
});
const coderabbit = checkCodeRabbit(repo, prNumber);
const riskyCodeTested = checkRiskyCodeTested(prData.files ?? []);
Expand Down Expand Up @@ -1832,7 +1895,7 @@ function main(): void {
baseRefOid: prData.baseRefOid,
headRefName: prData.headRefName,
baseRefName: prData.baseRefName,
headRepository: prData.headRepository.nameWithOwner,
headRepository,
},
currentRevision,
currentBaseSha,
Expand Down
Loading
Loading