diff --git a/.agents/skills/nemoclaw-maintainer-cut-release-tag/SKILL.md b/.agents/skills/nemoclaw-maintainer-cut-release-tag/SKILL.md index c91262804c..f7ef87f3a5 100644 --- a/.agents/skills/nemoclaw-maintainer-cut-release-tag/SKILL.md +++ b/.agents/skills/nemoclaw-maintainer-cut-release-tag/SKILL.md @@ -18,7 +18,17 @@ The release is one signed annotated semver tag on an already-merged `origin/main When a release admin creates or moves `lkg` to a commit carrying a `vX.Y.Z` tag, the `Release / LKG Brev Image` workflow dispatches the `Release Production Image` workflow in `brevdev/nemoclaw-image` on its `main` branch. The dispatch passes the immutable semver tag instead of the mutable `lkg` tag. The source workflow requires the `NEMOCLAW_IMAGE_DISPATCH_TOKEN` Actions secret with Actions read/write access to `brevdev/nemoclaw-image`; a missing secret fails before the API request, and the workflow summary never includes its value. -The trigger summary records the selected release tag, full commit SHA, target workflow, and dispatch result. +The trigger summary records the selected release tag, full commit SHA, target workflow, dispatch result, downstream run ID, and a direct link to the downstream run. +After `lkg` promotion, find and wait for the source trigger run using the promoted commit: + +```bash +gh run list --repo NVIDIA/NemoClaw --workflow release-lkg-brev-image.yaml --commit --event push --limit 1 --json databaseId,status,conclusion,url +gh run watch --repo NVIDIA/NemoClaw --exit-status +gh run view --repo NVIDIA/NemoClaw --log +``` + +Extract the exact `https://github.com/brevdev/nemoclaw-image/actions/runs/` URL printed by the source run, give that link to the maintainer immediately, and tell them to follow it to terminal success. +Treat dispatch acceptance as an intermediate state, not proof of production image promotion: the downstream run must succeed and its summary must show successful runtime E2E validation and promotion of the `nemoclaw-brev-cpu` image family. A rejected dispatch fails the trigger run but does not move or roll back `lkg`. Deleting `lkg` does not dispatch an image build. The downstream scheduled reconciliation remains available if the event-driven dispatch fails or is delayed. diff --git a/.github/workflows/release-lkg-brev-image.yaml b/.github/workflows/release-lkg-brev-image.yaml index d41b31bf41..ac1a190dfc 100644 --- a/.github/workflows/release-lkg-brev-image.yaml +++ b/.github/workflows/release-lkg-brev-image.yaml @@ -19,6 +19,7 @@ jobs: dispatch-production-image: if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.event.deleted == false }} runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Check out LKG target uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/scripts/release-lkg-brev-image.sh b/scripts/release-lkg-brev-image.sh index 32fc5ea13c..2f151e459b 100755 --- a/scripts/release-lkg-brev-image.sh +++ b/scripts/release-lkg-brev-image.sh @@ -12,6 +12,8 @@ SUMMARY_PATH="${GITHUB_STEP_SUMMARY:-/dev/null}" lkg_commit="unresolved" release_tag="none" dispatch_result="not attempted" +downstream_run_id="unavailable" +downstream_run_url="" write_summary() { { @@ -21,6 +23,13 @@ write_summary() { echo "- Release tag: \`$release_tag\`" echo "- Target: \`$TARGET_REPOSITORY/.github/workflows/$TARGET_WORKFLOW@$TARGET_REF\`" echo "- Dispatch result: \`$dispatch_result\`" + if [[ -n "$downstream_run_url" ]]; then + echo "- Downstream run: [$downstream_run_id]($downstream_run_url)" + echo + echo "Follow the downstream run to terminal success and verify production image promotion." + else + echo "- Downstream run: \`$downstream_run_id\`" + fi } >>"$SUMMARY_PATH" } @@ -70,21 +79,32 @@ if ! command -v gh >/dev/null 2>&1; then fail "GitHub CLI is required to dispatch $TARGET_REPOSITORY" fi -payload="$(printf '{"ref":"%s","inputs":{"nemoclaw_ref":"%s"}}' "$TARGET_REF" "$release_tag")" +payload="$(printf '{"ref":"%s","inputs":{"nemoclaw_ref":"%s"},"return_run_details":true}' "$TARGET_REF" "$release_tag")" endpoint="repos/$TARGET_REPOSITORY/actions/workflows/$TARGET_WORKFLOW/dispatches" -if ! printf '%s\n' "$payload" \ - | env -u GH_DEBUG GH_TOKEN="$NEMOCLAW_IMAGE_DISPATCH_TOKEN" gh api \ - --method POST \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "$endpoint" \ - --input - \ - >/dev/null; then +if ! dispatch_details="$( + printf '%s\n' "$payload" \ + | env -u GH_DEBUG GH_TOKEN="$NEMOCLAW_IMAGE_DISPATCH_TOKEN" gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + "$endpoint" \ + --input - \ + --jq '[.workflow_run_id, .html_url] | @tsv' +)"; then dispatch_result="rejected" fail "GitHub rejected the production image dispatch to $TARGET_REPOSITORY/$TARGET_WORKFLOW" fi -dispatch_result="accepted (HTTP 204)" -printf 'release-lkg-brev-image: dispatched %s for %s (%s)\n' \ - "$TARGET_WORKFLOW" "$release_tag" "$lkg_commit" +IFS=$'\t' read -r downstream_run_id downstream_run_url <<<"$dispatch_details" +expected_run_url="https://github.com/$TARGET_REPOSITORY/actions/runs/$downstream_run_id" +if [[ ! "$downstream_run_id" =~ ^[0-9]+$ || "$downstream_run_url" != "$expected_run_url" ]]; then + downstream_run_id="unavailable" + downstream_run_url="" + dispatch_result="rejected (invalid run details)" + fail "GitHub accepted the dispatch but did not return valid downstream run details" +fi + +dispatch_result="accepted (HTTP 200)" +printf 'release-lkg-brev-image: dispatched %s for %s (%s): %s\n' \ + "$TARGET_WORKFLOW" "$release_tag" "$lkg_commit" "$downstream_run_url" diff --git a/test/release-lkg-brev-image.test.ts b/test/release-lkg-brev-image.test.ts index 43fb69895a..9a23835476 100644 --- a/test/release-lkg-brev-image.test.ts +++ b/test/release-lkg-brev-image.test.ts @@ -27,6 +27,7 @@ type Workflow = { { if?: string; steps?: WorkflowStep[]; + "timeout-minutes"?: number; } >; on?: { @@ -95,6 +96,11 @@ if [[ "\${GH_EXIT_CODE:-0}" != "0" ]]; then echo "HTTP 403: dispatch denied" >&2 exit "$GH_EXIT_CODE" fi +if [[ -n "\${GH_OUTPUT+x}" ]]; then + printf '%s\n' "$GH_OUTPUT" +else + printf '123456789\thttps://github.com/brevdev/nemoclaw-image/actions/runs/123456789\n' +fi `, "utf8", ); @@ -156,14 +162,17 @@ describe("LKG production image dispatch", () => { "-H", "Accept: application/vnd.github+json", "-H", - "X-GitHub-Api-Version: 2022-11-28", + "X-GitHub-Api-Version: 2026-03-10", "repos/brevdev/nemoclaw-image/actions/workflows/build-scheduled.yml/dispatches", "--input", "-", + "--jq", + "[.workflow_run_id, .html_url] | @tsv", ]); expect(JSON.parse(fs.readFileSync(fixture.inputPath, "utf8"))).toEqual({ ref: "main", inputs: { nemoclaw_ref: "v0.0.10" }, + return_run_details: true, }); const summary = fs.readFileSync(fixture.summaryPath, "utf8"); expect(summary).toContain(`LKG commit: \`${fixture.commit}\``); @@ -171,7 +180,16 @@ describe("LKG production image dispatch", () => { expect(summary).toContain( "Target: `brevdev/nemoclaw-image/.github/workflows/build-scheduled.yml@main`", ); - expect(summary).toContain("Dispatch result: `accepted (HTTP 204)`"); + expect(summary).toContain("Dispatch result: `accepted (HTTP 200)`"); + expect(summary).toContain( + "Downstream run: [123456789](https://github.com/brevdev/nemoclaw-image/actions/runs/123456789)", + ); + expect(summary).toContain( + "Follow the downstream run to terminal success and verify production image promotion.", + ); + expect(result.stdout).toContain( + "https://github.com/brevdev/nemoclaw-image/actions/runs/123456789", + ); expect(`${result.stdout}${result.stderr}${summary}`).not.toContain("test-dispatch-token"); }); @@ -233,6 +251,40 @@ describe("LKG production image dispatch", () => { expect(fs.readFileSync(fixture.summaryPath, "utf8")).toContain("Dispatch result: `rejected`"); }); + it("fails when GitHub omits valid downstream run details (#6772)", () => { + const fixture = createFixture(); + tag(fixture, "v0.0.1"); + + const result = runDispatch(fixture, { GH_OUTPUT: "null\tnull" }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "GitHub accepted the dispatch but did not return valid downstream run details", + ); + const summary = fs.readFileSync(fixture.summaryPath, "utf8"); + expect(summary).toContain("Dispatch result: `rejected (invalid run details)`"); + expect(summary).toContain("Downstream run: `unavailable`"); + expect(summary).not.toContain("actions/runs/"); + }); + + it("rejects a downstream run URL that does not match its numeric ID (#6772)", () => { + const fixture = createFixture(); + tag(fixture, "v0.0.1"); + + const result = runDispatch(fixture, { + GH_OUTPUT: "123456789\thttps://github.com/brevdev/nemoclaw-image/actions/runs/987654321", + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "GitHub accepted the dispatch but did not return valid downstream run details", + ); + const summary = fs.readFileSync(fixture.summaryPath, "utf8"); + expect(summary).toContain("Dispatch result: `rejected (invalid run details)`"); + expect(summary).toContain("Downstream run: `unavailable`"); + expect(summary).not.toContain("actions/runs/"); + }); + // source-shape-contract: security -- The secret-bearing LKG trigger must stay canonical, deletion-safe, read-only, and immutable it("keeps LKG dispatch inside the trusted secret boundary (#6772)", () => { const workflow = readYaml(".github/workflows/release-lkg-brev-image.yaml"); @@ -245,6 +297,7 @@ describe("LKG production image dispatch", () => { expect(job.if).toBe( "${{ github.repository == 'NVIDIA/NemoClaw' && github.event.deleted == false }}", ); + expect(job["timeout-minutes"]).toBe(5); expect(checkout?.uses).toMatch(/^actions\/checkout@[0-9a-f]{40}$/u); expect(checkout?.with).toEqual({ ref: "${{ github.sha }}",