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
33 changes: 33 additions & 0 deletions .github/workflows/e2e-advisor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,19 @@ jobs:
--schema "$ADVISOR_DIR/tools/e2e-advisor/schema.json" \
--out-dir "$GITHUB_WORKSPACE/artifacts/e2e-advisor"

- name: Run deterministic scenario E2E advisor
id: scenario-analysis
continue-on-error: true
env:
BASE_REF: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && format('target/{0}', inputs.target_base) || inputs.base_ref) }}
HEAD_REF: ${{ github.event_name == 'pull_request' && 'HEAD' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'HEAD' || inputs.head_ref) }}
run: |
cd "$ADVISOR_WORKDIR"
node --experimental-strip-types "$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" \
--base "$BASE_REF" \
--head "$HEAD_REF" \
--out-dir "$GITHUB_WORKSPACE/artifacts/e2e-advisor"

Comment on lines +176 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard scenario advisor script invocation in trusted-main checkout.

Line 184 runs "$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" unconditionally. In this workflow, ADVISOR_DIR is checked out from main; when that file is not present there yet, the step fails (Cannot find module .../scenarios.mts), which matches the pipeline failure log.

Suggested fix
       - name: Run deterministic scenario E2E advisor
         id: scenario-analysis
         continue-on-error: true
         env:
           BASE_REF: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && format('target/{0}', inputs.target_base) || inputs.base_ref) }}
           HEAD_REF: ${{ github.event_name == 'pull_request' && 'HEAD' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'HEAD' || inputs.head_ref) }}
         run: |
           cd "$ADVISOR_WORKDIR"
-          node --experimental-strip-types "$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" \
-            --base "$BASE_REF" \
-            --head "$HEAD_REF" \
-            --out-dir "$GITHUB_WORKSPACE/artifacts/e2e-advisor"
+          if [ -f "$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" ]; then
+            node --experimental-strip-types "$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" \
+              --base "$BASE_REF" \
+              --head "$HEAD_REF" \
+              --out-dir "$GITHUB_WORKSPACE/artifacts/e2e-advisor"
+          else
+            echo "Skipping deterministic scenario advisor: trusted main checkout does not yet contain scenarios.mts"
+          fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Run deterministic scenario E2E advisor
id: scenario-analysis
continue-on-error: true
env:
BASE_REF: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && format('target/{0}', inputs.target_base) || inputs.base_ref) }}
HEAD_REF: ${{ github.event_name == 'pull_request' && 'HEAD' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'HEAD' || inputs.head_ref) }}
run: |
cd "$ADVISOR_WORKDIR"
node --experimental-strip-types "$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" \
--base "$BASE_REF" \
--head "$HEAD_REF" \
--out-dir "$GITHUB_WORKSPACE/artifacts/e2e-advisor"
- name: Run deterministic scenario E2E advisor
id: scenario-analysis
continue-on-error: true
env:
BASE_REF: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && format('target/{0}', inputs.target_base) || inputs.base_ref) }}
HEAD_REF: ${{ github.event_name == 'pull_request' && 'HEAD' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'HEAD' || inputs.head_ref) }}
run: |
cd "$ADVISOR_WORKDIR"
if [ -f "$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" ]; then
node --experimental-strip-types "$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" \
--base "$BASE_REF" \
--head "$HEAD_REF" \
--out-dir "$GITHUB_WORKSPACE/artifacts/e2e-advisor"
else
echo "Skipping deterministic scenario advisor: trusted main checkout does not yet contain scenarios.mts"
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e-advisor.yaml around lines 176 - 188, The step invoking
"$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" should be guarded so it only runs
when that file actually exists in the trusted checkout; update the run block in
the step with id scenario-analysis to test for the presence of
"$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" (e.g., using a shell conditional
like test -f or [ -f ]), and only call node --experimental-strip-types
"$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts" --base "$BASE_REF" --head
"$HEAD_REF" --out-dir "$GITHUB_WORKSPACE/artifacts/e2e-advisor" when the file is
present, otherwise print a clear skip message and exit successfully.

- name: Publish job summary
if: always()
run: |
Expand All @@ -181,6 +194,9 @@ jobs:
else
printf '# E2E Recommendation Advisor\n\nAdvisor analysis did not produce a summary. See raw artifacts/logs.\n' >> "$GITHUB_STEP_SUMMARY"
fi
if [ -f "$GITHUB_WORKSPACE/artifacts/e2e-advisor/e2e-scenario-advisor-summary.md" ]; then
cat "$GITHUB_WORKSPACE/artifacts/e2e-advisor/e2e-scenario-advisor-summary.md" >> "$GITHUB_STEP_SUMMARY"
fi

- name: Auto-dispatch required E2E jobs
if: ${{ always() && github.event_name == 'pull_request' }}
Expand Down Expand Up @@ -231,6 +247,23 @@ jobs:
echo "Skipping E2E advisor comment: trusted main checkout does not contain comment.mts or comment.mjs"
fi

- name: Post E2E scenario advisor PR comment
if: ${{ always() && github.event_name == 'pull_request' }}
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.E2E_ADVISOR_GITHUB_TOKEN || github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [ -f "$GITHUB_WORKSPACE/artifacts/e2e-advisor/e2e-scenario-advisor-summary.md" ] && [ -f "$ADVISOR_DIR/tools/e2e-advisor/scenario-comment.mts" ]; then
node --experimental-strip-types "$ADVISOR_DIR/tools/e2e-advisor/scenario-comment.mts" \
--repo "$GITHUB_REPOSITORY" \
--pr "$PR_NUMBER" \
--summary "$GITHUB_WORKSPACE/artifacts/e2e-advisor/e2e-scenario-advisor-summary.md" \
--result "$GITHUB_WORKSPACE/artifacts/e2e-advisor/e2e-scenario-advisor-result.json"
else
echo "Skipping E2E scenario advisor comment: summary or scenario-comment.mts missing"
fi

- name: Upload advisor artifacts
if: always()
uses: actions/upload-artifact@v4
Expand Down
94 changes: 94 additions & 0 deletions test/e2e-scenario-advisor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";

import { buildScenarioComment } from "../tools/e2e-advisor/scenario-comment.mts";
import {
analyzeScenarioRecommendations,
renderScenarioSummary,
} from "../tools/e2e-advisor/scenarios.mts";

const ROOT = new URL("../", import.meta.url).pathname;

function analyze(changedFiles: string[]) {
return analyzeScenarioRecommendations({
baseRef: "origin/main",
headRef: "HEAD",
changedFiles,
root: ROOT,
});
}

describe("E2E scenario advisor", () => {
it("requires all scenario E2E when the all-scenarios workflow changes", () => {
const result = analyze([".github/workflows/e2e-scenarios-all.yaml"]);

expect(result.required).toEqual([
expect.objectContaining({
id: "e2e-scenarios-all",
workflow: "e2e-scenarios-all.yaml",
dispatchCommand:
"gh workflow run e2e-scenarios-all.yaml --ref <pr-head-ref>",
}),
]);
expect(result.noScenarioE2eReason).toBeNull();
});

it("requires targeted scenario E2E when a validation suite changes", () => {
const result = analyze([
"test/e2e/validation_suites/messaging/telegram/00-telegram-injection-safety.sh",
]);

expect(result.required).toContainEqual(
expect.objectContaining({
id: "ubuntu-repo-docker__cloud-nvidia-openclaw-telegram:messaging-telegram",
workflow: "e2e-scenarios.yaml",
scenario: "ubuntu-repo-docker__cloud-nvidia-openclaw-telegram",
suiteFilter: "messaging-telegram",
}),
);
});

it("requires all scenario E2E and targeted follow-up when suite metadata changes", () => {
const result = analyze([
"test/e2e/validation_suites/suites.yaml",
"test/e2e/validation_suites/messaging/telegram/00-telegram-injection-safety.sh",
]);

expect(result.required).toContainEqual(
expect.objectContaining({ id: "e2e-scenarios-all" }),
);
expect(result.required).toContainEqual(
expect.objectContaining({
id: "ubuntu-repo-docker__cloud-nvidia-openclaw-telegram:messaging-telegram",
scenario: "ubuntu-repo-docker__cloud-nvidia-openclaw-telegram",
suiteFilter: "messaging-telegram",
}),
);
});

it("does not recommend scenario E2E for unrelated files", () => {
const result = analyze(["docs/reference/commands.mdx"]);

expect(result.required).toEqual([]);
expect(result.optional).toEqual([]);
expect(result.noScenarioE2eReason).toMatch(/No scenario workflow/);
});

it("renders a summary and second sticky scenario comment", () => {
const result = analyze([".github/workflows/e2e-scenarios.yaml"]);
const summary = renderScenarioSummary(result);
const comment = buildScenarioComment({
summary,
result,
runUrl: "https://example.invalid/run",
});

expect(summary).toContain("# E2E Scenario Advisor");
expect(comment).toContain("<!-- nemoclaw-e2e-scenario-advisor -->");
expect(comment).toContain("## E2E Scenario Advisor Recommendation");
expect(comment).toContain("`e2e-scenarios-all`");
expect(comment).toContain("https://example.invalid/run");
});
});
114 changes: 114 additions & 0 deletions tools/e2e-advisor/scenario-comment.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { pathToFileURL } from "node:url";

import { upsertStickyComment } from "../advisors/github.mts";
import { parseArgs, readIfExists, readJsonIfExists } from "../advisors/io.mts";

import type {
ScenarioAdvisorResult,
ScenarioRecommendation,
} from "./scenarios.mts";

export const SCENARIO_ADVISOR_MARKER = "<!-- nemoclaw-e2e-scenario-advisor -->";

if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
}

async function main(): Promise<void> {
const args = parseArgs(process.argv.slice(2));
const repo = args.repo || process.env.GITHUB_REPOSITORY;
const pr = args.pr || process.env.PR_NUMBER;
const summaryPath =
args.summary || "artifacts/e2e-advisor/e2e-scenario-advisor-summary.md";
const resultPath =
args.result || "artifacts/e2e-advisor/e2e-scenario-advisor-result.json";
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
const runUrl =
process.env.GITHUB_SERVER_URL &&
process.env.GITHUB_REPOSITORY &&
process.env.GITHUB_RUN_ID
? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
: undefined;

if (!repo || !pr) {
console.log(
"Skipping E2E scenario advisor comment: repo or PR number not provided",
);
return;
}
if (!token) {
console.log(
"Skipping E2E scenario advisor comment: GITHUB_TOKEN/GH_TOKEN not provided",
);
return;
}

const summary = readIfExists(summaryPath);
if (!summary) {
throw new Error(`No scenario advisor summary found at ${summaryPath}`);
}

const result = readJsonIfExists<ScenarioAdvisorResult>(resultPath);
const body = buildScenarioComment({ summary, result, runUrl });

await upsertStickyComment({
repo,
pr,
token,
marker: SCENARIO_ADVISOR_MARKER,
body,
label: "E2E scenario advisor",
userAgent: "nemoclaw-e2e-scenario-advisor",
});
}

export function buildScenarioComment({
summary,
result,
runUrl,
marker = SCENARIO_ADVISOR_MARKER,
}: {
summary: string;
result?: ScenarioAdvisorResult;
runUrl?: string;
marker?: string;
}): string {
const required = Array.isArray(result?.required) ? result.required : [];
const optional = Array.isArray(result?.optional) ? result.optional : [];
const requiredLine = recommendationLine(required);
const optionalLine = recommendationLine(optional);
const dispatch =
required.length > 0
? `\n\n**Dispatch required scenario E2E:**\n${required.map((item) => `- \`${item.dispatchCommand}\``).join("\n")}`
: "";
const run = runUrl ? `\n\n[Workflow run](${runUrl})` : "";

return `${marker}
## E2E Scenario Advisor Recommendation

**Required scenario E2E:** ${requiredLine}
**Optional scenario E2E:** ${optionalLine}${dispatch}${run}

<details>
<summary>Full scenario advisor summary</summary>

${summary.trim()}

</details>
`;
}

function recommendationLine(recommendations: ScenarioRecommendation[]): string {
return recommendations.length > 0
? recommendations.map((item) => `\`${item.id}\``).join(", ")
: "_None_";
}
Loading
Loading