From 1ec2aa82091d2512e3237336a242080698560745 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 3 Aug 2026 02:39:59 +0800 Subject: [PATCH] fix(release): require requested validation children (#118031) * fix(release): require requested validation children * fix(release): bind Telegram reuse evidence --- .github/workflows/full-release-validation.yml | 44 ++++++++- .../workflows/qa-live-transports-convex.yml | 7 ++ .../find-reusable-release-validation.sh | 12 ++- scripts/release-ci-summary.d.mts | 5 +- scripts/release-ci-summary.mjs | 97 +++++++++++-------- .../find-reusable-release-validation.test.ts | 82 +++++++++++++++- .../package-acceptance-workflow.test.ts | 50 +++++++++- test/scripts/release-ci-summary.test.ts | 38 +++++++- 8 files changed, 282 insertions(+), 53 deletions(-) diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index 5a811a13e53f..67a0ed600ab1 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -308,6 +308,10 @@ jobs: RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} + NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} + NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }} + NPM_TELEGRAM_SCENARIO: ${{ inputs.npm_telegram_scenario }} + ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }} run: | set -euo pipefail # Lane-selection inputs must match the prior run's manifest exactly; @@ -321,6 +325,10 @@ jobs: --arg releasePackageSpec "$RELEASE_PACKAGE_SPEC" \ --arg packageAcceptancePackageSpec "$PACKAGE_ACCEPTANCE_PACKAGE_SPEC" \ --arg codexPluginSpec "$CODEX_PLUGIN_SPEC" \ + --arg npmTelegramPackageSpec "$NPM_TELEGRAM_PACKAGE_SPEC" \ + --arg npmTelegramProviderMode "$NPM_TELEGRAM_PROVIDER_MODE" \ + --arg npmTelegramScenario "$NPM_TELEGRAM_SCENARIO" \ + --arg allowUnreleasedChangelog "$ALLOW_UNRELEASED_CHANGELOG" \ '{ provider: $provider, mode: $mode, @@ -329,7 +337,11 @@ jobs: crossOsSuiteFilter: $crossOsSuiteFilter, releasePackageSpec: $releasePackageSpec, packageAcceptancePackageSpec: $packageAcceptancePackageSpec, - codexPluginSpec: $codexPluginSpec + codexPluginSpec: $codexPluginSpec, + npmTelegramPackageSpec: $npmTelegramPackageSpec, + npmTelegramProviderMode: $npmTelegramProviderMode, + npmTelegramScenario: $npmTelegramScenario, + allowUnreleasedChangelog: $allowUnreleasedChangelog }')" bash workflow/scripts/github/find-reusable-release-validation.sh \ --target-sha "$TARGET_SHA" \ @@ -937,8 +949,8 @@ jobs: npm_telegram: name: Run package Telegram E2E - needs: [resolve_target] - if: ${{ always() && needs.resolve_target.result == 'success' && inputs.rerun_group == 'npm-telegram' && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '') }} + needs: [resolve_target, evidence_reuse] + if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group) && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '') && needs.evidence_reuse.outputs.reuse != 'true' }} continue-on-error: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }} runs-on: ubuntu-24.04 timeout-minutes: ${{ inputs.release_profile == 'full' && 360 || 60 }} @@ -1019,6 +1031,7 @@ jobs: DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT: ${{ needs.docker_runtime_assets_preflight.result }} PREPARE_RELEASE_CANDIDATE_RESULT: ${{ needs.prepare_release_candidate.result }} RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} + NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} EVIDENCE_REUSE: ${{ needs.evidence_reuse.outputs.reuse }} EVIDENCE_ROOT_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_root_run_id }} @@ -1289,8 +1302,12 @@ jobs: normal_ci_required=0 plugin_prerelease_required=0 release_checks_required=0 + npm_telegram_required=0 performance_required=0 candidate_required=0 + if [[ "$RERUN_GROUP" == "npm-telegram" || ( "$RERUN_GROUP" == "all" && ( -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" || -n "${RELEASE_PACKAGE_SPEC// }" ) ) ]]; then + npm_telegram_required=1 + fi if [[ -z "${RELEASE_PACKAGE_SPEC// }" && -z "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then case "$RERUN_GROUP" in all|plugin-prerelease|release-checks|cross-os|live-e2e|package) @@ -1306,6 +1323,11 @@ jobs: # Lanes were skipped because a prior green validation covers this # target; re-verify the chain-root run and its recorded child runs # so evidence that went stale after resolution cannot pass. + reused_npm_telegram_run_id="$(jq -r '.childRuns.npmTelegram // ""' <<< "$EVIDENCE_MANIFEST")" + if [[ "$npm_telegram_required" == "1" && -z "${reused_npm_telegram_run_id// }" ]]; then + echo "::error::Reused evidence did not record the required npm Telegram child run." + failed=1 + fi evidence_state="$(gh_with_retry run view "$EVIDENCE_ROOT_RUN_ID" --json status,conclusion --jq '(.status // "") + "/" + (.conclusion // "")')" if [[ "$evidence_state" != "completed/success" ]]; then echo "::error::Reused evidence run ${EVIDENCE_ROOT_RUN_ID} is ${evidence_state}; evidence is no longer valid." @@ -1389,7 +1411,7 @@ jobs: fi if [[ "$NPM_TELEGRAM_RESULT" == "skipped" && -z "${NPM_TELEGRAM_RUN_ID// }" ]]; then - check_child "npm_telegram" "" 0 || failed=1 + check_child "npm_telegram" "" "$npm_telegram_required" || failed=1 elif [[ "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then check_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID" 0 || echo "::warning::npm_telegram is advisory for Tideclaw alpha validation." else @@ -1534,6 +1556,10 @@ jobs: RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} + NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} + NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }} + NPM_TELEGRAM_SCENARIO: ${{ inputs.npm_telegram_scenario }} + ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }} run: | set -euo pipefail manifest_dir="${RUNNER_TEMP}/full-release-validation" @@ -1605,6 +1631,10 @@ jobs: --arg releasePackageSpec "$RELEASE_PACKAGE_SPEC" \ --arg packageAcceptancePackageSpec "$PACKAGE_ACCEPTANCE_PACKAGE_SPEC" \ --arg codexPluginSpec "$CODEX_PLUGIN_SPEC" \ + --arg npmTelegramPackageSpec "$NPM_TELEGRAM_PACKAGE_SPEC" \ + --arg npmTelegramProviderMode "$NPM_TELEGRAM_PROVIDER_MODE" \ + --arg npmTelegramScenario "$NPM_TELEGRAM_SCENARIO" \ + --arg allowUnreleasedChangelog "$ALLOW_UNRELEASED_CHANGELOG" \ '{ version: 3, workflowName: $workflowName, @@ -1627,7 +1657,11 @@ jobs: crossOsSuiteFilter: $crossOsSuiteFilter, releasePackageSpec: $releasePackageSpec, packageAcceptancePackageSpec: $packageAcceptancePackageSpec, - codexPluginSpec: $codexPluginSpec + codexPluginSpec: $codexPluginSpec, + npmTelegramPackageSpec: $npmTelegramPackageSpec, + npmTelegramProviderMode: $npmTelegramProviderMode, + npmTelegramScenario: $npmTelegramScenario, + allowUnreleasedChangelog: $allowUnreleasedChangelog }, controls: { stableSoakRequired: ($releaseProfile == "stable" or $releaseProfile == "full"), diff --git a/.github/workflows/qa-live-transports-convex.yml b/.github/workflows/qa-live-transports-convex.yml index 943276d8d0e5..43b64c7b529e 100644 --- a/.github/workflows/qa-live-transports-convex.yml +++ b/.github/workflows/qa-live-transports-convex.yml @@ -587,6 +587,13 @@ jobs: retention-days: 14 if-no-files-found: error + - name: Require requested Buzz QA runner + if: always() && steps.resolve_buzz.outcome == 'success' && steps.resolve_buzz.outputs.available != 'true' + shell: bash + run: | + echo "::error::The selected ref does not declare the requested Buzz QA runner." + exit 1 + run_live_telegram: name: Run Telegram live QA lane with Convex leases needs: [authorize_actor, validate_selected_ref] diff --git a/scripts/github/find-reusable-release-validation.sh b/scripts/github/find-reusable-release-validation.sh index 806e93bdf39a..91e05f428c9d 100755 --- a/scripts/github/find-reusable-release-validation.sh +++ b/scripts/github/find-reusable-release-validation.sh @@ -253,7 +253,17 @@ for ((index = 0; index < run_count; index += 1)); do and (.verifier.schemaVersion == 3) and (.verifier.sourceSha == $verifier_sha) and ([.children[].role] | sort) == - ["normalCi", "pluginPrerelease", "productPerformance", "releaseChecks"] + (if ( + .rerunGroup == "all" + and ( + ((.validationInputs.npmTelegramPackageSpec // "") | length) > 0 + or ((.validationInputs.releasePackageSpec // "") | length) > 0 + ) + ) then + ["normalCi", "npmTelegram", "pluginPrerelease", "productPerformance", "releaseChecks"] + else + ["normalCi", "pluginPrerelease", "productPerformance", "releaseChecks"] + end) and ([.children[].runId] | length == (unique | length)) and ([.children[] | select(.role == "productPerformance") diff --git a/scripts/release-ci-summary.d.mts b/scripts/release-ci-summary.d.mts index ba74f1cf119f..d33f1174f16e 100644 --- a/scripts/release-ci-summary.d.mts +++ b/scripts/release-ci-summary.d.mts @@ -39,7 +39,10 @@ export function expectedChildDispatches( trustedRef: string; workflow: string; }[]; -export function requiredChildKeysForRerunGroup(rerunGroup: unknown): Set; +export function requiredChildKeysForRerunGroup( + rerunGroup: unknown, + validationInputs?: Record, +): Set; export function expectedSelectedChildDispatches( parentRunId: unknown, parentRunAttempt: unknown, diff --git a/scripts/release-ci-summary.mjs b/scripts/release-ci-summary.mjs index e9b3aeb79782..0e13a588bf36 100755 --- a/scripts/release-ci-summary.mjs +++ b/scripts/release-ci-summary.mjs @@ -180,12 +180,22 @@ export function expectedChildDispatches(parentRunId, parentRunAttempt, parentWor })); } -export function requiredChildKeysForRerunGroup(rerunGroup) { +export function requiredChildKeysForRerunGroup(rerunGroup, validationInputs = {}) { const childKeys = RERUN_GROUP_CHILD_KEYS.get(rerunGroup); if (!childKeys) { throw new Error(`release validation manifest rerun group is invalid: ${rerunGroup}`); } - return new Set(childKeys); + const selectedKeys = new Set(childKeys); + if ( + rerunGroup === "all" && + ((typeof validationInputs.npmTelegramPackageSpec === "string" && + validationInputs.npmTelegramPackageSpec.length > 0) || + (typeof validationInputs.releasePackageSpec === "string" && + validationInputs.releasePackageSpec.length > 0)) + ) { + selectedKeys.add("npmTelegram"); + } + return selectedKeys; } export function expectedSelectedChildDispatches( @@ -1356,7 +1366,10 @@ export function validateReleaseRunEvidence( ); } } - const selectedKeys = requiredChildKeysForRerunGroup(rootEvidence.manifest.rerunGroup); + const selectedKeys = requiredChildKeysForRerunGroup( + rootEvidence.manifest.rerunGroup, + rootEvidence.manifest.validationInputs, + ); const expectedChildren = expectedSelectedChildDispatches( rootEvidence.manifest.runId, rootEvidence.manifest.runAttempt, @@ -1750,51 +1763,53 @@ async function main() { ); } + const selectedKeys = requiredChildKeysForRerunGroup( + sourceManifest.rerunGroup, + sourceManifest.validationInputs, + ); const expectedChildren = expectedSelectedChildDispatches( sourceManifest.runId, sourceManifest.runAttempt, sourceManifest.workflowRef, - requiredChildKeysForRerunGroup(sourceManifest.rerunGroup), + selectedKeys, ); const sourceParentJobs = findParentJobsAll(sourceManifest.runId, repository); - children = manifestChildEntries( - sourceManifest, - expectedChildren, - requiredChildKeysForRerunGroup(sourceManifest.rerunGroup), - ).map(({ child, runId: childRunId }) => { - const run = githubRestJson(`actions/runs/${childRunId}`, repository); - const originAttempt = resolveManifestChildOriginAttempt( - run, - child, - sourceManifest, - sourceParentJobs, - ); - if (originAttempt === undefined) { - throw new Error(`manifest child dispatch tuple mismatch: ${child.name}`); - } - const parentJob = selectManifestParentJob( - sourceParentJobs, - child, - sourceManifest, - originAttempt, - ); - const validatedRun = validateManifestChildRun( - run, - child, - childRunId, - { ...sourceManifest, workflowSha: sourceParent.headSha }, - sourceParentJobs, - parentJobLog(parentJob.id, repository), - repository, - ); - if (child.manifestKey === "productPerformance") { - validatePerformanceArtifactOnlyJobs( - findParentJobsAll(childRunId, repository), - run.run_attempt, + children = manifestChildEntries(sourceManifest, expectedChildren, selectedKeys).map( + ({ child, runId: childRunId }) => { + const run = githubRestJson(`actions/runs/${childRunId}`, repository); + const originAttempt = resolveManifestChildOriginAttempt( + run, + child, + sourceManifest, + sourceParentJobs, ); - } - return { child, run: validatedRun }; - }); + if (originAttempt === undefined) { + throw new Error(`manifest child dispatch tuple mismatch: ${child.name}`); + } + const parentJob = selectManifestParentJob( + sourceParentJobs, + child, + sourceManifest, + originAttempt, + ); + const validatedRun = validateManifestChildRun( + run, + child, + childRunId, + { ...sourceManifest, workflowSha: sourceParent.headSha }, + sourceParentJobs, + parentJobLog(parentJob.id, repository), + repository, + ); + if (child.manifestKey === "productPerformance") { + validatePerformanceArtifactOnlyJobs( + findParentJobsAll(childRunId, repository), + run.run_attempt, + ); + } + return { child, run: validatedRun }; + }, + ); } else { console.log("candidate-sha: unavailable (release validation manifest not uploaded)"); if (parent.status === "completed" && parent.conclusion === "success") { diff --git a/test/scripts/find-reusable-release-validation.test.ts b/test/scripts/find-reusable-release-validation.test.ts index ebb3455e6f00..576545d5e474 100644 --- a/test/scripts/find-reusable-release-validation.test.ts +++ b/test/scripts/find-reusable-release-validation.test.ts @@ -26,6 +26,10 @@ const DEFAULT_INPUTS = { releasePackageSpec: "", packageAcceptancePackageSpec: "", codexPluginSpec: "", + npmTelegramPackageSpec: "", + npmTelegramProviderMode: "mock-openai", + npmTelegramScenario: "", + allowUnreleasedChangelog: "false", }; interface ParentTuple { @@ -210,6 +214,10 @@ function normalizedEvidence(options: { const shaPinned = workflowRef.startsWith("release-ci/"); const validationInputs = options.validationInputs === undefined ? DEFAULT_INPUTS : options.validationInputs; + const npmTelegramRequired = + validationInputs !== null && + (validationInputs.npmTelegramPackageSpec.length > 0 || + validationInputs.releasePackageSpec.length > 0); const manifest = { version: shaPinned ? 3 : 2, workflowName: "Full Release Validation", @@ -232,7 +240,7 @@ function normalizedEvidence(options: { }, childRuns: { normalCi: "201", - npmTelegram: "", + npmTelegram: npmTelegramRequired ? "205" : "", pluginPrerelease: "202", releaseChecks: "203", productPerformance: { @@ -292,6 +300,19 @@ function normalizedEvidence(options: { "openclaw-release-checks.yml", "-release-checks", ], + ...(npmTelegramRequired + ? ([ + [ + "npmTelegram", + "205", + 1, + 2, + "NPM Telegram Beta E2E", + "npm-telegram-beta-e2e.yml", + "-npm-telegram", + ], + ] as const) + : []), ["productPerformance", "204", 3, 2, "OpenClaw Performance", "openclaw-performance.yml", ""], ] as const; const children = roles.map( @@ -551,6 +572,33 @@ describe("scripts/github/find-reusable-release-validation.sh", () => { }); }); + it("reuses npm Telegram evidence only when its selectors match exactly", () => { + const { clone, priorSha } = getSharedRepo(); + const validationInputs = { + ...DEFAULT_INPUTS, + npmTelegramPackageSpec: "openclaw@2026.7.2-beta.7", + npmTelegramProviderMode: "live-frontier", + npmTelegramScenario: "telegram-status-command", + }; + const record = normalizedEvidence({ targetSha: priorSha, validationInputs }); + const { binDir, fixtures, validatorPath } = setUpFixtures([{ record, runId: "111" }]); + + const result = runResolver({ + binDir, + fixtures, + inputs: validationInputs, + repoDir: clone, + targetSha: priorSha, + validatorPath, + }); + + expect(result.status).toBe(0); + expect(parseOutput(result.stdout)).toMatchObject({ + evidence_run_id: "111", + reuse: "true", + }); + }); + it("rejects noncanonical release refs and workflow SHAs outside trusted main", () => { const { clone, priorSha } = getSharedRepo(); const record = normalizedEvidence({ targetSha: priorSha }); @@ -827,6 +875,38 @@ describe("scripts/github/find-reusable-release-validation.sh", () => { recordOptions: { validationInputs: { ...DEFAULT_INPUTS, provider: "anthropic" } }, resolverOptions: {}, }, + { + expected: "validation inputs differ", + label: "different npm Telegram package", + recordOptions: { + validationInputs: { ...DEFAULT_INPUTS, npmTelegramPackageSpec: "openclaw@old" }, + }, + resolverOptions: {}, + }, + { + expected: "validation inputs differ", + label: "different npm Telegram provider mode", + recordOptions: { + validationInputs: { ...DEFAULT_INPUTS, npmTelegramProviderMode: "live-frontier" }, + }, + resolverOptions: {}, + }, + { + expected: "validation inputs differ", + label: "different npm Telegram scenario", + recordOptions: { + validationInputs: { ...DEFAULT_INPUTS, npmTelegramScenario: "telegram-status-command" }, + }, + resolverOptions: {}, + }, + { + expected: "validation inputs differ", + label: "different unreleased changelog policy", + recordOptions: { + validationInputs: { ...DEFAULT_INPUTS, allowUnreleasedChangelog: "true" }, + }, + resolverOptions: {}, + }, { expected: "soak false differs from true", label: "missing required soak", diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index a99ed0d1532e..bf000e917e2d 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -3173,6 +3173,14 @@ describe("package artifact reuse", () => { expect(workflowStep(buzzJob, "Upload Buzz QA artifacts").with?.path).toBe( "${{ steps.resolve_buzz.outputs.output_dir }}", ); + const requireBuzz = workflowStep(buzzJob, "Require requested Buzz QA runner"); + expect(requireBuzz.if).toBe( + "always() && steps.resolve_buzz.outcome == 'success' && steps.resolve_buzz.outputs.available != 'true'", + ); + expect(requireBuzz.run).toContain( + "The selected ref does not declare the requested Buzz QA runner.", + ); + expect(requireBuzz.run).toContain("exit 1"); }); it("runs live transport lanes nightly while release checks stay gated", () => { @@ -3576,22 +3584,41 @@ describe("package artifact reuse", () => { it("runs full release children from the trusted workflow ref", () => { const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); + const evidenceReuseJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "evidence_reuse"); const npmTelegramJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "npm_telegram"); const performanceJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance"); + const summaryJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary"); + const evidenceReuseStep = workflowStep(evidenceReuseJob, "Find reusable validation evidence"); const dispatchStep = workflowStep(npmTelegramJob, "Dispatch and monitor npm Telegram E2E"); + const manifestStep = workflowStep(summaryJob, "Write release validation manifest"); expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}"); expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1'); expect(npmTelegramJob.name).toBe("Run package Telegram E2E"); - expect(npmTelegramJob.needs).toEqual(["resolve_target"]); + expect(npmTelegramJob.needs).toEqual(["resolve_target", "evidence_reuse"]); expect(npmTelegramJob["timeout-minutes"]).toBe( "${{ inputs.release_profile == 'full' && 360 || 60 }}", ); expect(performanceJob["timeout-minutes"]).toBe( "${{ inputs.release_profile == 'full' && 360 || 120 }}", ); - expect(npmTelegramJob.if).toContain("inputs.rerun_group == 'npm-telegram'"); - expect(npmTelegramJob.if).not.toContain("inputs.rerun_group == 'all'"); + expect(npmTelegramJob.if).toContain( + 'contains(fromJSON(\'["all","npm-telegram"]\'), inputs.rerun_group)', + ); + expect(npmTelegramJob.if).toContain("needs.evidence_reuse.outputs.reuse != 'true'"); + expect(evidenceReuseStep.env).toMatchObject({ + ALLOW_UNRELEASED_CHANGELOG: + "${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }}", + NPM_TELEGRAM_PACKAGE_SPEC: "${{ inputs.npm_telegram_package_spec }}", + NPM_TELEGRAM_PROVIDER_MODE: "${{ inputs.npm_telegram_provider_mode }}", + NPM_TELEGRAM_SCENARIO: "${{ inputs.npm_telegram_scenario }}", + }); + expectTextToIncludeAll(evidenceReuseStep.run, [ + "npmTelegramPackageSpec: $npmTelegramPackageSpec", + "npmTelegramProviderMode: $npmTelegramProviderMode", + "npmTelegramScenario: $npmTelegramScenario", + "allowUnreleasedChangelog: $allowUnreleasedChangelog", + ]); expect(dispatchStep.env).toEqual({ CHILD_WORKFLOW_KIND: "npm-telegram", CHILD_WORKFLOW_REF: "${{ github.ref_name }}", @@ -3603,6 +3630,19 @@ describe("package artifact reuse", () => { SCENARIO: "${{ inputs.npm_telegram_scenario }}", TARGET_SHA: "${{ needs.resolve_target.outputs.sha }}", }); + expect(manifestStep.env).toMatchObject({ + ALLOW_UNRELEASED_CHANGELOG: + "${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }}", + NPM_TELEGRAM_PACKAGE_SPEC: "${{ inputs.npm_telegram_package_spec }}", + NPM_TELEGRAM_PROVIDER_MODE: "${{ inputs.npm_telegram_provider_mode }}", + NPM_TELEGRAM_SCENARIO: "${{ inputs.npm_telegram_scenario }}", + }); + expectTextToIncludeAll(manifestStep.run, [ + "npmTelegramPackageSpec: $npmTelegramPackageSpec", + "npmTelegramProviderMode: $npmTelegramProviderMode", + "npmTelegramScenario: $npmTelegramScenario", + "allowUnreleasedChangelog: $allowUnreleasedChangelog", + ]); expectTextToIncludeAll(dispatchStep.run, [ 'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-npm-telegram"', 'dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"', @@ -3626,6 +3666,10 @@ describe("package artifact reuse", () => { "Verify release checks accepted Tideclaw alpha advisory lanes", "release_checks_advisory_only", "release_check_blocking_job", + 'if [[ "$RERUN_GROUP" == "npm-telegram" || ( "$RERUN_GROUP" == "all"', + "npm_telegram_required=1", + "Reused evidence did not record the required npm Telegram child run.", + 'check_child "npm_telegram" "" "$npm_telegram_required"', 'if [[ "$RELEASE_PROFILE" == "beta" && "$1" == "Run package acceptance / Telegram package acceptance / "* ]]; then', 'or (.name | startswith("Run QA Lab runtime-pair lane ("))', 'or .name == "Run QA Lab live Discord lane"', diff --git a/test/scripts/release-ci-summary.test.ts b/test/scripts/release-ci-summary.test.ts index 15a903941b57..cf728747f791 100644 --- a/test/scripts/release-ci-summary.test.ts +++ b/test/scripts/release-ci-summary.test.ts @@ -236,10 +236,14 @@ function rawManifest({ runReleaseSoak: "false", targetSha, validationInputs: { + allowUnreleasedChangelog: "false", codexPluginSpec: "", crossOsSuiteFilter: "", liveSuiteFilter: "", mode: "direct", + npmTelegramPackageSpec: "", + npmTelegramProviderMode: "mock-openai", + npmTelegramScenario: "", packageAcceptancePackageSpec: "", provider: "openai", releasePackageSpec: "", @@ -1308,6 +1312,35 @@ describe("release CI summary child correlation", () => { ); }); + it("requires the npm Telegram child for all-validation with an effective package spec", () => { + const raw = rawManifest({}); + raw.childRuns.npmTelegram = "505"; + raw.validationInputs.npmTelegramPackageSpec = "openclaw@beta"; + const manifest = validateParentManifest(raw, { + runAttempt: 2, + runId: "29090000000", + }); + const selected = requiredChildKeysForRerunGroup(manifest.rerunGroup, manifest.validationInputs); + expect([...selected].sort()).toEqual([ + "normalCi", + "npmTelegram", + "pluginPrerelease", + "productPerformance", + "releaseChecks", + ]); + const missing = { + ...manifest, + childRunIds: { ...manifest.childRunIds, npmTelegram: "" }, + }; + expect(() => + manifestChildEntries( + missing, + expectedChildDispatches(manifest.runId, manifest.runAttempt, "main"), + selected, + ), + ).toThrow("selected child is missing from manifest: NPM Telegram Beta E2E"); + }); + it("keeps historical non-reuse v2 manifests readable without validation inputs", () => { const legacy = rawManifest({}); delete (legacy as { validationInputs?: unknown }).validationInputs; @@ -1573,7 +1606,10 @@ describe("release CI summary child correlation", () => { ); const mismatchedRoot = { ...root, - validationInputs: { ...root.validationInputs, provider: "anthropic" }, + validationInputs: { + ...root.validationInputs, + npmTelegramScenario: "telegram-status-command", + }, }; expect(() => validateEvidenceReuseChain(current, mismatchedRoot, mismatchedRoot)).toThrow(