From e38fcb254b25913ef9fe1591e32e9fb8238e07a6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 00:14:45 +0100 Subject: [PATCH] test: strengthen release workflow contract coverage --- .../package-acceptance-workflow.test.ts | 146 ++++++++++++++++-- 1 file changed, 130 insertions(+), 16 deletions(-) diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 779a6e135eb..30635762696 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -1,5 +1,6 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; const PACKAGE_ACCEPTANCE_WORKFLOW = ".github/workflows/package-acceptance.yml"; const LIVE_E2E_WORKFLOW = ".github/workflows/openclaw-live-and-e2e-checks-reusable.yml"; @@ -10,6 +11,50 @@ const FULL_RELEASE_VALIDATION_WORKFLOW = ".github/workflows/full-release-validat const QA_LIVE_TRANSPORTS_WORKFLOW = ".github/workflows/qa-live-transports-convex.yml"; const UPGRADE_SURVIVOR_RUN_SCRIPT = "scripts/e2e/lib/upgrade-survivor/run.sh"; +type WorkflowStep = { + env?: Record; + if?: string; + name?: string; + run?: string; + uses?: string; + with?: Record; +}; + +type WorkflowJob = { + env?: Record; + if?: string; + name?: string; + needs?: string | string[]; + steps?: WorkflowStep[]; +}; + +type Workflow = { + jobs?: Record; +}; + +function readWorkflow(path: string): Workflow { + return parse(readFileSync(path, "utf8")) as Workflow; +} + +function workflowJob(path: string, jobName: string): WorkflowJob { + const job = readWorkflow(path).jobs?.[jobName]; + expect(job, `expected workflow job ${jobName}`).toBeDefined(); + return job!; +} + +function workflowStep(job: WorkflowJob, stepName: string): WorkflowStep { + const step = job.steps?.find((candidate) => candidate.name === stepName); + expect(step, `expected workflow step ${stepName}`).toBeDefined(); + return step!; +} + +function expectTextToIncludeAll(text: string | undefined, snippets: string[]): void { + expect(text).toBeDefined(); + for (const snippet of snippets) { + expect(text).toContain(snippet); + } +} + describe("package acceptance workflow", () => { it("resolves candidate package sources before reusing Docker E2E lanes", () => { const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); @@ -447,32 +492,101 @@ describe("package artifact reuse", () => { it("runs full release children from the trusted workflow ref", () => { const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); + const npmTelegramJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "npm_telegram"); + const dispatchStep = workflowStep(npmTelegramJob, "Dispatch and monitor npm Telegram E2E"); expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}"); expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@"'); - expect(workflow).toContain( + expect(npmTelegramJob.name).toBe("Run package Telegram E2E"); + expect(npmTelegramJob.needs).toEqual(["resolve_target", "release_checks"]); + expect(npmTelegramJob.if).toContain( + "inputs.rerun_group == 'all' && inputs.release_profile == 'full'", + ); + expect(dispatchStep.env).toMatchObject({ + RELEASE_CHECKS_RUN_ID: "${{ needs.release_checks.outputs.run_id }}", + TARGET_SHA: "${{ needs.resolve_target.outputs.sha }}", + }); + expectTextToIncludeAll(dispatchStep.run, [ 'gh workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}"', - ); - expect(workflow).toContain('-f harness_ref="$TARGET_SHA"'); - expect(workflow).toContain("needs: [resolve_target, release_checks]"); - expect(workflow).toContain("inputs.rerun_group == 'all' && inputs.release_profile == 'full'"); - expect(workflow).toContain("RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }}"); - expect(workflow).toContain("-f package_artifact_name=release-package-under-test"); - expect(workflow).toContain('-f package_artifact_run_id="$RELEASE_CHECKS_RUN_ID"'); - expect(workflow).toContain('-f package_label="full-release-${TARGET_SHA:0:12}"'); - expect(workflow).toContain("child_rerun_group=all"); - expect(workflow).toContain('-f rerun_group="$child_rerun_group"'); - expect(workflow).toContain('args+=(-f live_suite_filter="$LIVE_SUITE_FILTER")'); - expect(workflow).toContain( + '-f harness_ref="$TARGET_SHA"', + 'args=(-f package_spec="${PACKAGE_SPEC:-openclaw@beta}"', + 'if [[ -z "${PACKAGE_SPEC// }" ]]; then', + "-f package_artifact_name=release-package-under-test", + '-f package_artifact_run_id="$RELEASE_CHECKS_RUN_ID"', + '-f package_label="full-release-${TARGET_SHA:0:12}"', + 'args+=(-f scenario="$SCENARIO")', + ]); + expectTextToIncludeAll(workflow, [ + "child_rerun_group=all", + '-f rerun_group="$child_rerun_group"', + 'args+=(-f live_suite_filter="$LIVE_SUITE_FILTER")', "cancel-in-progress: ${{ inputs.ref == 'main' && inputs.rerun_group == 'all' }}", - ); - expect(workflow).toContain("gh run cancel"); + "gh run cancel", + "NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}", + ]); expect(workflow).not.toContain("force-cancel"); - expect(workflow).toContain("NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}"); expect(workflow).not.toContain("workflow_ref:"); expect(workflow).not.toContain("inputs.workflow_ref"); }); + it("documents the full-release Telegram package path in operator summaries", () => { + const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); + const releaseDocs = readFileSync("docs/reference/RELEASING.md", "utf8"); + const fullReleaseDocs = readFileSync("docs/reference/full-release-validation.md", "utf8"); + + expectTextToIncludeAll(workflow, [ + "Published-package Telegram E2E:", + "Package Telegram E2E: release package artifact from \\`OpenClaw Release Checks\\`", + "Package Telegram E2E: skipped unless \\`release_profile=full\\` or \\`npm_telegram_package_spec\\` is provided", + ]); + expect(releaseDocs).toContain( + "Focused `npm-telegram` reruns require `npm_telegram_package_spec`", + ); + expectTextToIncludeAll(fullReleaseDocs, [ + "full pre-publish candidate", + "silently skip that", + "Telegram package lane", + "| `npm-telegram` | Published-package Telegram E2E; requires `npm_telegram_package_spec`. |", + ]); + }); + + it("lets npm Telegram consume current-run or release-run package artifacts", () => { + const job = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e"); + const currentRunDownload = workflowStep(job, "Download package-under-test artifact"); + const releaseRunDownload = workflowStep( + job, + "Download package-under-test artifact from release run", + ); + const validateStep = workflowStep(job, "Validate inputs and secrets"); + const runStep = workflowStep(job, "Run package Telegram E2E"); + + expect(currentRunDownload).toMatchObject({ + if: "inputs.package_artifact_name != '' && inputs.package_artifact_run_id == ''", + uses: "actions/download-artifact@v8", + with: { + name: "${{ inputs.package_artifact_name }}", + path: ".artifacts/telegram-package-under-test", + }, + }); + expect(releaseRunDownload).toMatchObject({ + if: "inputs.package_artifact_name != '' && inputs.package_artifact_run_id != ''", + uses: "actions/download-artifact@v8", + with: { + "github-token": "${{ github.token }}", + name: "${{ inputs.package_artifact_name }}", + path: ".artifacts/telegram-package-under-test", + "run-id": "${{ inputs.package_artifact_run_id }}", + }, + }); + expectTextToIncludeAll(validateStep.run, [ + 'if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then', + "package_spec must be openclaw@beta", + ]); + expectTextToIncludeAll(runStep.run, [ + 'export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgzs[0]}"', + ]); + }); + it("keeps release QA and repo E2E lanes off scarce 32-core runners", () => { const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8"); const qaWorkflow = readFileSync(QA_LIVE_TRANSPORTS_WORKFLOW, "utf8");