-
Notifications
You must be signed in to change notification settings - Fork 2.8k
ci(e2e): add scenario advisor comment #4004
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+744
−0
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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_"; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard scenario advisor script invocation in trusted-main checkout.
Line 184 runs
"$ADVISOR_DIR/tools/e2e-advisor/scenarios.mts"unconditionally. In this workflow,ADVISOR_DIRis checked out frommain; 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
🤖 Prompt for AI Agents