diff --git a/.github/workflows/openclaw-release-telegram-qa.yml b/.github/workflows/openclaw-release-telegram-qa.yml index 1a2bcb802453..2c6a87592f5a 100644 --- a/.github/workflows/openclaw-release-telegram-qa.yml +++ b/.github/workflows/openclaw-release-telegram-qa.yml @@ -2772,6 +2772,16 @@ jobs: actions: read contents: read steps: + # Identity binds this checkout to the workflow definition that received + # the OIDC token. Do not execute status code from the release candidate. + - name: Checkout trusted Telegram status helper + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + repository: ${{ needs.trusted_identity.outputs.workflow_repository }} + ref: ${{ needs.trusted_identity.outputs.workflow_sha }} + fetch-depth: 1 + persist-credentials: false + - name: Record advisory status id: record_status env: @@ -2796,119 +2806,7 @@ jobs: shell: bash run: | set -euo pipefail - status=failure - if [[ "$IDENTITY_RESULT" == "cancelled" || - "$BUILD_RESULT" == "cancelled" || - "$ATTESTATION_RESULT" == "cancelled" || - "$RUN_RESULT" == "cancelled" ]]; then - status=cancelled - elif [[ "$IDENTITY_STATUS" == "success" && - "$BUILD_STATUS" == "success" && - "$ATTESTATION_STATUS" == "success" && - "$EXECUTION_STATUS" == "success" ]]; then - status=success - fi - - candidate_artifact=null - if [[ "$CANDIDATE_ARTIFACT_ID" =~ ^[1-9][0-9]*$ && - "$CANDIDATE_ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ && - "$ARCHIVE_NAME" == "release-telegram-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${TARGET_SHA}.tar.zst" && - "$ARCHIVE_SHA256" =~ ^[a-f0-9]{64}$ && - -n "$CANDIDATE_VERSION" ]]; then - candidate_artifact="$( - jq -cn \ - --arg id "$CANDIDATE_ARTIFACT_ID" \ - --arg name "$ARCHIVE_NAME" \ - --arg digest "$CANDIDATE_ARTIFACT_DIGEST" \ - --arg runId "$GITHUB_RUN_ID" \ - --argjson runAttempt "$GITHUB_RUN_ATTEMPT" \ - --arg fileName "$ARCHIVE_NAME" \ - --arg sha256 "$ARCHIVE_SHA256" \ - --arg sourceSha "$TARGET_SHA" \ - --arg version "$CANDIDATE_VERSION" \ - '{ - id: $id, - name: $name, - digest: $digest, - runId: $runId, - runAttempt: $runAttempt, - fileName: $fileName, - sha256: $sha256, - sourceSha: $sourceSha, - version: $version - }' - )" - fi - if [[ "$status" == "success" ]]; then - [[ "$candidate_artifact" != "null" && - "$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && - "$TARGET_SHA" =~ ^[a-f0-9]{40}$ && - "$EVIDENCE_ARTIFACT_ID" =~ ^[1-9][0-9]*$ && - "$EVIDENCE_ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ && - "$EVIDENCE_ARTIFACT_NAME" == "release-qa-live-telegram-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${TARGET_SHA}" ]] - fi - - status_dir=".artifacts/release-check-status" - status_file="${status_dir}/qa_live_telegram_release_checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.env" - evidence_file="${status_dir}/qa_live_telegram_release_checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json" - mkdir -p "$status_dir" - step_outcomes="identity:${IDENTITY_STATUS} build:${BUILD_STATUS} attest:${ATTESTATION_STATUS} execute:${EXECUTION_STATUS}" - { - printf 'run_id=%s\n' "$GITHUB_RUN_ID" - printf 'run_attempt=%s\n' "$GITHUB_RUN_ATTEMPT" - printf 'target_sha=%s\n' "$TARGET_SHA" - printf 'workflow_sha=%s\n' "$WORKFLOW_SHA" - printf 'job=%s\n' "qa_live_telegram_release_checks" - printf 'variant=\n' - printf 'status=%s\n' "$status" - printf 'job_status=%s\n' "$RUN_RESULT" - printf 'step_outcomes=%s\n' "$step_outcomes" - } >"$status_file" - jq -n \ - --arg status "$status" \ - --arg jobStatus "$RUN_RESULT" \ - --arg identityOutcome "identity:${IDENTITY_STATUS}" \ - --arg buildOutcome "build:${BUILD_STATUS}" \ - --arg attestOutcome "attest:${ATTESTATION_STATUS}" \ - --arg executeOutcome "execute:${EXECUTION_STATUS}" \ - --arg runId "$GITHUB_RUN_ID" \ - --argjson runAttempt "$GITHUB_RUN_ATTEMPT" \ - --arg workflowSha "$WORKFLOW_SHA" \ - --arg targetSha "$TARGET_SHA" \ - --arg evidenceId "$EVIDENCE_ARTIFACT_ID" \ - --arg evidenceName "$EVIDENCE_ARTIFACT_NAME" \ - --arg evidenceDigest "$EVIDENCE_ARTIFACT_DIGEST" \ - --argjson candidateArtifact "$candidate_artifact" \ - '{ - version: 1, - kind: "release-check-status", - job: "qa_live_telegram_release_checks", - status: $status, - jobStatus: $jobStatus, - stepOutcomes: [ - $identityOutcome, - $buildOutcome, - $attestOutcome, - $executeOutcome - ], - runId: $runId, - runAttempt: $runAttempt, - workflowSha: $workflowSha, - targetSha: $targetSha, - evidenceArtifact: { - id: $evidenceId, - name: $evidenceName, - digest: $evidenceDigest, - runId: $runId, - runAttempt: $runAttempt - }, - candidateArtifact: $candidateArtifact - }' >"$evidence_file" - { - echo "status=$status" - echo "status_file=$status_file" - echo "evidence_file=$evidence_file" - } >>"$GITHUB_OUTPUT" + node scripts/release-telegram-qa.mjs advisory-status - name: Upload advisory status if: always() diff --git a/scripts/release-telegram-qa.mjs b/scripts/release-telegram-qa.mjs new file mode 100644 index 000000000000..6cbea93baa14 --- /dev/null +++ b/scripts/release-telegram-qa.mjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node +// Trusted helpers for the release Telegram workflow. Keep policy here so it can +// be exercised directly rather than inferred from a workflow shell snippet. +import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"; + +const SHA = /^[a-f0-9]{40}$/u; +const DIGEST = /^[a-f0-9]{64}$/u; +const POSITIVE_ID = /^[1-9][0-9]*$/u; + +function required(name) { + const value = process.env[name]; + if (value === undefined) { + throw new Error(`${name} is required`); + } + return value; +} + +function appendOutput(lines) { + appendFileSync(required("GITHUB_OUTPUT"), `${lines.join("\n")}\n`, "utf8"); +} + +function advisoryStatus() { + const env = process.env; + const cancelled = [ + env.IDENTITY_RESULT, + env.BUILD_RESULT, + env.ATTESTATION_RESULT, + env.RUN_RESULT, + ].includes("cancelled"); + const succeeded = [ + env.IDENTITY_STATUS, + env.BUILD_STATUS, + env.ATTESTATION_STATUS, + env.EXECUTION_STATUS, + ].every((value) => value === "success"); + const status = cancelled ? "cancelled" : succeeded ? "success" : "failure"; + const runId = required("GITHUB_RUN_ID"); + const runAttempt = required("GITHUB_RUN_ATTEMPT"); + const targetSha = required("TARGET_SHA"); + const candidateArtifact = + POSITIVE_ID.test(env.CANDIDATE_ARTIFACT_ID ?? "") && + DIGEST.test(env.CANDIDATE_ARTIFACT_DIGEST ?? "") && + env.ARCHIVE_NAME === `release-telegram-candidate-${runId}-${runAttempt}-${targetSha}.tar.zst` && + DIGEST.test(env.ARCHIVE_SHA256 ?? "") && + Boolean(env.CANDIDATE_VERSION) + ? { + id: env.CANDIDATE_ARTIFACT_ID, + name: env.ARCHIVE_NAME, + digest: env.CANDIDATE_ARTIFACT_DIGEST, + runId, + runAttempt: Number(runAttempt), + fileName: env.ARCHIVE_NAME, + sha256: env.ARCHIVE_SHA256, + sourceSha: targetSha, + version: env.CANDIDATE_VERSION, + } + : null; + if ( + status === "success" && + (!candidateArtifact || + !SHA.test(env.WORKFLOW_SHA ?? "") || + !SHA.test(targetSha) || + !POSITIVE_ID.test(env.EVIDENCE_ARTIFACT_ID ?? "") || + !DIGEST.test(env.EVIDENCE_ARTIFACT_DIGEST ?? "") || + env.EVIDENCE_ARTIFACT_NAME !== `release-qa-live-telegram-${runId}-${runAttempt}-${targetSha}`) + ) { + throw new Error("Successful Telegram release status has incomplete evidence."); + } + const statusDir = ".artifacts/release-check-status"; + const fileStem = `qa_live_telegram_release_checks-${runId}-${runAttempt}`; + const statusFile = `${statusDir}/${fileStem}.env`; + const evidenceFile = `${statusDir}/${fileStem}.json`; + const stepOutcomes = [ + `identity:${env.IDENTITY_STATUS ?? ""}`, + `build:${env.BUILD_STATUS ?? ""}`, + `attest:${env.ATTESTATION_STATUS ?? ""}`, + `execute:${env.EXECUTION_STATUS ?? ""}`, + ]; + mkdirSync(statusDir, { recursive: true }); + writeFileSync( + statusFile, + [ + `run_id=${runId}`, + `run_attempt=${runAttempt}`, + `target_sha=${targetSha}`, + `workflow_sha=${env.WORKFLOW_SHA ?? ""}`, + "job=qa_live_telegram_release_checks", + "variant=", + `status=${status}`, + `job_status=${env.RUN_RESULT ?? ""}`, + `step_outcomes=${stepOutcomes.join(" ")}`, + "", + ].join("\n"), + "utf8", + ); + writeFileSync( + evidenceFile, + `${JSON.stringify( + { + version: 1, + kind: "release-check-status", + job: "qa_live_telegram_release_checks", + status, + jobStatus: env.RUN_RESULT ?? "", + stepOutcomes, + runId, + runAttempt: Number(runAttempt), + workflowSha: env.WORKFLOW_SHA ?? "", + targetSha, + evidenceArtifact: { + id: env.EVIDENCE_ARTIFACT_ID ?? "", + name: env.EVIDENCE_ARTIFACT_NAME ?? "", + digest: env.EVIDENCE_ARTIFACT_DIGEST ?? "", + runId, + runAttempt: Number(runAttempt), + }, + candidateArtifact, + }, + null, + 2, + )}\n`, + "utf8", + ); + appendOutput([`status=${status}`, `status_file=${statusFile}`, `evidence_file=${evidenceFile}`]); +} + +const command = process.argv[2]; +try { + if (command === "advisory-status") { + advisoryStatus(); + } else { + throw new Error(`Unknown release Telegram QA command: ${command ?? ""}`); + } +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +} diff --git a/test/scripts/openclaw-release-telegram-qa-workflow.test.ts b/test/scripts/openclaw-release-telegram-qa-workflow.test.ts index aa614317953e..1fd33744923b 100644 --- a/test/scripts/openclaw-release-telegram-qa-workflow.test.ts +++ b/test/scripts/openclaw-release-telegram-qa-workflow.test.ts @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { mkdirSync, readdirSync, readFileSync, utimesSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, utimesSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { parse } from "yaml"; @@ -7,8 +7,19 @@ import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; const RELEASE_CHECKS_PATH = ".github/workflows/openclaw-release-checks.yml"; const WORKFLOW_PATH = ".github/workflows/openclaw-release-telegram-qa.yml"; +const HELPER = "scripts/release-telegram-qa.mjs"; const tempDirs = useAutoCleanupTempDirTracker(afterEach); +type WorkflowStep = { + env?: Record; + id?: string; + if?: string; + name?: string; + run?: string; + uses?: string; + with?: Record; +}; + type WorkflowJob = { "continue-on-error"?: boolean; environment?: string; @@ -18,36 +29,45 @@ type WorkflowJob = { permissions?: Record; "runs-on"?: unknown; "timeout-minutes"?: unknown; - steps?: Array<{ - env?: Record; - if?: string; - name?: string; - run?: string; - uses?: string; - with?: Record; - "working-directory"?: string; - }>; - uses?: string; - with?: Record; + steps?: WorkflowStep[]; }; -function workflowJob(name: string): WorkflowJob { - const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as { - jobs?: Record; - }; - const job = workflow.jobs?.[name]; - if (!job) { - throw new Error(`Expected workflow job ${name}`); - } - return job; +function workflow(path = WORKFLOW_PATH) { + return parse(readFileSync(path, "utf8")) as { jobs?: Record }; } -function workflowStep(job: WorkflowJob, name: string) { - const step = job.steps?.find((candidate) => candidate.name === name); - if (!step) { - throw new Error(`Expected workflow step ${name}`); +function job(name: string, path = WORKFLOW_PATH): WorkflowJob { + const value = workflow(path).jobs?.[name]; + if (!value) { + throw new Error(`Expected workflow job ${name}`); } - return step; + return value; +} + +function step(jobName: string, name: string, path = WORKFLOW_PATH): WorkflowStep { + const value = job(jobName, path).steps?.find((candidate) => candidate.name === name); + if (!value) { + throw new Error(`Expected ${jobName} step ${name}`); + } + return value; +} + +function requireRun(jobName: string, name: string): string { + const value = step(jobName, name).run; + if (!value) { + throw new Error(`Expected ${jobName} step ${name} to run a script`); + } + return value; +} + +function extractHereDocument(script: string, delimiter: string): string { + const match = script.match( + new RegExp(`<<'${delimiter}'\\n([\\s\\S]*?)\\n${delimiter}(?:\\n|$)`, "u"), + ); + if (!match?.[1]) { + throw new Error(`Expected ${delimiter} heredoc`); + } + return match[1]; } function runIdentityVerification(params: { @@ -66,14 +86,11 @@ function runIdentityVerification(params: { : `${repository}/.github/workflows/openclaw-release-checks.yml@refs/heads/release-ci/test`; const workflowRefName = invocation === "dispatch" ? "refs/heads/main" : "refs/heads/release-ci/test"; - const targetSha = "a".repeat(40); const workdir = tempDirs.make("openclaw-telegram-identity-"); const fakeBin = join(workdir, "bin"); - const curlPath = join(fakeBin, "curl"); const githubOutput = join(workdir, "github-output"); mkdirSync(fakeBin); const workflowSha = params.workflowSha ?? params.expectedTrustedWorkflowSha; - const oidcWorkflowSha = params.oidcWorkflowSha ?? workflowSha; const payload = { aud: "openclaw-release-telegram-qa", event_name: "workflow_dispatch", @@ -89,67 +106,63 @@ function runIdentityVerification(params: { runner_environment: "github-hosted", sha: workflowSha, workflow_ref: workflowRef, - workflow_sha: oidcWorkflowSha, + workflow_sha: params.oidcWorkflowSha ?? workflowSha, }; - const token = [ - Buffer.from("{}").toString("base64url"), - Buffer.from(JSON.stringify(payload)).toString("base64url"), - "signature", - ].join("."); - writeFileSync(curlPath, "#!/usr/bin/env bash\nprintf '%s\\n' \"$FAKE_OIDC_JSON\"\n", { - mode: 0o755, - }); - const script = workflowStep( - workflowJob("trusted_identity"), - "Verify dispatched-main identity", - ).run; - if (!script) { - throw new Error("Expected trusted identity script"); - } - return spawnSync("bash", ["-c", script], { - cwd: workdir, - encoding: "utf8", - env: { - ACTIONS_ID_TOKEN_REQUEST_TOKEN: "test-token", - ACTIONS_ID_TOKEN_REQUEST_URL: "https://example.invalid/oidc?", - CALLER_WORKFLOW_REF: workflowRef, - CALLER_WORKFLOW_SHA: workflowSha, - EXPECTED_TRUSTED_WORKFLOW_SHA: params.expectedTrustedWorkflowSha, - FAKE_OIDC_JSON: JSON.stringify({ value: token }), - GITHUB_EVENT_NAME: "workflow_dispatch", - GITHUB_OUTPUT: githubOutput, - GITHUB_REF: workflowRefName, - GITHUB_REPOSITORY: repository, - GITHUB_SHA: workflowSha, - JOB_CONTEXT: JSON.stringify({ - workflow_ref: trustedWorkflowRef, - workflow_repository: repository, - workflow_sha: params.expectedTrustedWorkflowSha, - }), - PATH: `${fakeBin}:${process.env.PATH}`, - TARGET_REF: "refs/heads/release/2026.7.1", - TARGET_SHA: targetSha, - WORKFLOW_REF: workflowRef, - WORKFLOW_SHA: workflowSha, + const token = ["{}", JSON.stringify(payload), "signature"] + .map((part) => Buffer.from(part).toString("base64url")) + .join("."); + writeFileSync( + join(fakeBin, "curl"), + "#!/usr/bin/env bash\nprintf '%s\\n' \"$FAKE_OIDC_JSON\"\n", + { + mode: 0o755, }, - }); + ); + return spawnSync( + "bash", + ["-c", requireRun("trusted_identity", "Verify dispatched-main identity")], + { + cwd: workdir, + encoding: "utf8", + env: { + ...process.env, + ACTIONS_ID_TOKEN_REQUEST_TOKEN: "test-token", + ACTIONS_ID_TOKEN_REQUEST_URL: "https://example.invalid/oidc?", + CALLER_WORKFLOW_REF: workflowRef, + CALLER_WORKFLOW_SHA: workflowSha, + EXPECTED_TRUSTED_WORKFLOW_SHA: params.expectedTrustedWorkflowSha, + FAKE_OIDC_JSON: JSON.stringify({ value: token }), + GITHUB_EVENT_NAME: "workflow_dispatch", + GITHUB_OUTPUT: githubOutput, + GITHUB_REF: workflowRefName, + GITHUB_REPOSITORY: repository, + GITHUB_SHA: workflowSha, + JOB_CONTEXT: JSON.stringify({ + workflow_ref: trustedWorkflowRef, + workflow_repository: repository, + workflow_sha: params.expectedTrustedWorkflowSha, + }), + PATH: `${fakeBin}:${process.env.PATH}`, + TARGET_REF: "refs/heads/release/2026.7.1", + TARGET_SHA: "a".repeat(40), + WORKFLOW_REF: workflowRef, + WORKFLOW_SHA: workflowSha, + }, + }, + ); } function runAdvisoryStatus(overrides: Record = {}) { const runId = "123456"; const runAttempt = "1"; const targetSha = "a".repeat(40); - const workflowSha = "b".repeat(40); const workdir = tempDirs.make("openclaw-telegram-advisory-status-"); const githubOutput = join(workdir, "github-output"); - const script = workflowStep(workflowJob("advisory_status"), "Record advisory status").run; - if (!script) { - throw new Error("Expected advisory status script"); - } - const result = spawnSync("bash", ["-c", script], { + const result = spawnSync(process.execPath, [join(process.cwd(), HELPER), "advisory-status"], { cwd: workdir, encoding: "utf8", env: { + ...process.env, ARCHIVE_NAME: `release-telegram-candidate-${runId}-${runAttempt}-${targetSha}.tar.zst`, ARCHIVE_SHA256: "c".repeat(64), ATTESTATION_RESULT: "success", @@ -168,916 +181,277 @@ function runAdvisoryStatus(overrides: Record = {}) { GITHUB_RUN_ID: runId, IDENTITY_RESULT: "success", IDENTITY_STATUS: "success", - PATH: process.env.PATH, RUN_RESULT: "success", TARGET_SHA: targetSha, - WORKFLOW_SHA: workflowSha, + WORKFLOW_SHA: "b".repeat(40), ...overrides, }, }); const output = result.status === 0 ? readFileSync(githubOutput, "utf8") : ""; - const status = output.match(/^status=(.*)$/mu)?.[1] ?? ""; - const requireScript = workflowStep( - workflowJob("advisory_status"), - "Require successful Telegram release check", - ).run; - if (!requireScript) { - throw new Error("Expected terminal Telegram status script"); - } - const requireResult = spawnSync("bash", ["-c", requireScript], { - cwd: workdir, - encoding: "utf8", - env: { - PATH: process.env.PATH, - STATUS: status, - }, - }); + const outputs = Object.fromEntries( + output + .trim() + .split("\n") + .filter(Boolean) + .map((line) => line.split("=", 2) as [string, string]), + ); const statusFile = join( workdir, ".artifacts", "release-check-status", `qa_live_telegram_release_checks-${runId}-${runAttempt}.env`, ); + const evidenceFile = statusFile.replace(/\.env$/u, ".json"); return { - output, - recordResult: result, - requireResult, - status, + evidence: result.status === 0 ? JSON.parse(readFileSync(evidenceFile, "utf8")) : null, + outputs, + result, statusFile: result.status === 0 ? readFileSync(statusFile, "utf8") : "", }; } -describe("release Telegram QA workflow", () => { - it("retries transient GitHub API responses during both provenance checks", () => { - const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as { - env?: Record; - }; - expect(workflow.env?.GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN).toContain( - "invalid character .* looking for beginning of value", - ); - - for (const [jobName, stepName] of [ - ["build_candidate", "Validate candidate release provenance"], - ["run_telegram", "Revalidate candidate release provenance"], - ] as const) { - const script = workflowStep(workflowJob(jobName), stepName).run; - expect(script).toContain("gh_with_retry()"); - expect(script).toContain("for attempt in 1 2 3 4 5"); - expect(script).toContain('stdout="$(gh "$@" 2>"$stderr_file")"'); - expect(script).toContain('cat "$stderr_file" >&2'); - expect(script).not.toContain('output="$(gh "$@" 2>&1)"'); - expect(script).toContain("gh_with_retry api \\\n"); - expect(script).toContain("gh_with_retry api graphql"); - } - }); - - it("attributes GitHub web-flow and unsigned release merges to their exact maintainer merger", () => { - const source = readFileSync(WORKFLOW_PATH, "utf8"); - - expect(source.match(/associatedPullRequests\(first:100\)/gu)).toHaveLength(2); - expect(source.match(/headRefOid == \$sha/gu)).toHaveLength(2); - expect(source.match(/\.headRepository\.nameWithOwner == \$repo/gu)).toHaveLength(2); - expect(source).not.toContain("commits/${candidate_sha}/pulls"); - expect(source.match(/if \.signature == null then "missing"/gu)).toHaveLength(2); - expect(source.match(/\$signature_status" == "invalid"/gu)).toHaveLength(2); - expect( - source.match(/\$signature_status" == "missing" \|\| "\$signer" == "web-flow"/gu), - ).toHaveLength(2); - expect(source.match(/\.mergeCommit\.oid == \$sha/gu)).toHaveLength(2); - expect(source.match(/\.baseRefName == \$base/gu)).toHaveLength(2); - expect(source.match(/\.baseRepository\.nameWithOwner == \$repo/gu)).toHaveLength(2); - expect(source.match(/\.mergedBy\.login\] \| unique \| select\(length == 1\)/gu)).toHaveLength( - 2, - ); - expect(source.match(/collaborators\/\$\{permission_actor\}\/permission/gu)).toHaveLength(2); - expect((source.match(/extended-stable\/\[0-9\]/gu) ?? []).length).toBeGreaterThanOrEqual(2); - expect(source).not.toContain("collaborators/${signer}/permission"); - }); - - it("resolves only candidate-specific provenance refs without fetching histories", () => { - const source = readFileSync(WORKFLOW_PATH, "utf8"); - - expect(source.match(/branches-where-head/gu)).toHaveLength(2); - expect(source.match(/gh_with_retry api --paginate/gu)).toHaveLength(2); - expect( - source.match(/git(?: -C \.candidate)? ls-remote --exit-code --refs origin/gu), - ).toHaveLength(2); - expect( - source.match(/git(?: -C \.candidate)? ls-remote origin 'refs\/tags\/v\*'/gu), - ).toHaveLength(2); - expect(source).not.toContain("'+refs/heads/release/*:refs/remotes/origin/release/*'"); - expect(source).not.toContain( - "'+refs/heads/extended-stable/*:refs/remotes/origin/extended-stable/*'", - ); - expect(source).not.toContain("'+refs/tags/v*:refs/tags/v*'"); - }); - - it("dispatches one accepted trusted-main child from release checks", () => { - const releaseSource = readFileSync(RELEASE_CHECKS_PATH, "utf8"); - const reusableSource = readFileSync(WORKFLOW_PATH, "utf8"); - const releaseWorkflow = parse(releaseSource) as { - jobs?: Record; - }; - const caller = releaseWorkflow.jobs?.qa_live_telegram_release_checks; - - expect(caller?.needs).toEqual(["resolve_target"]); - expect(caller?.if).toContain("needs.resolve_target.outputs.qa_live_telegram_enabled == 'true'"); - expect(caller?.permissions).toEqual({ - actions: "write", - contents: "read", - }); - expect(caller?.["timeout-minutes"]).toBe(210); - expect(caller?.["continue-on-error"]).toBeUndefined(); - expect(caller?.["runs-on"]).toBe("ubuntu-24.04"); - expect(caller?.environment).toBeUndefined(); - const dispatch = caller?.steps?.find( - (step) => step.name === "Dispatch and await trusted Telegram QA", - ); - expect(dispatch?.run).toContain('gh workflow run "$workflow"'); - expect(dispatch?.run).toContain('--repo "$GITHUB_REPOSITORY"'); - expect(dispatch?.run).toContain("-F event=workflow_dispatch"); - expect(dispatch?.run).toContain(".display_title == env.RUN_NAME"); - expect(dispatch?.run).toContain("$(openssl rand -hex 16)"); - expect(dispatch?.run).toContain("trap cancel_child_on_failure EXIT"); - expect(dispatch?.run).toContain("for _ in $(seq 1 6)"); - expect(dispatch?.run).toContain("/actions/runs/${run_id}/cancel"); - expect(dispatch?.run).toContain("for dispatch_attempt in 1 2 3 4 5"); - expect(dispatch?.run).toContain('gh api "repos/${GITHUB_REPOSITORY}/commits/main"'); - expect(dispatch?.run).toContain( - 'if [[ "$child_head_sha" == "$expected_trusted_workflow_sha" ]]; then', - ); - expect(dispatch?.run).toContain("Trusted main moved from"); - expect(dispatch?.run).toContain("Trusted main kept moving"); - expect(dispatch?.run).toContain("for _ in $(seq 1 1080)"); - expect(dispatch?.run).toContain("Trusted Telegram QA concluded ${conclusion}"); - expect( - releaseSource.match( - /"qa_live_telegram_release_checks=\$\{QA_LIVE_TELEGRAM_RELEASE_CHECKS_RESULT\}"/gu, - ), - ).toHaveLength(1); - expect(releaseSource).not.toContain( - "qa_live_matrix_release_checks|qa_live_telegram_release_checks|qa_live_discord_release_checks", - ); - expect(releaseSource).not.toContain("persist-credentials: true"); - - const dispatchers = readdirSync(".github/workflows") - .filter((name) => name.endsWith(".yml")) - .flatMap((name) => { - const path = `.github/workflows/${name}`; - return readFileSync(path, "utf8").includes('workflow="openclaw-release-telegram-qa.yml"') - ? [path] - : []; - }); - expect(dispatchers).toEqual([RELEASE_CHECKS_PATH]); - expect(reusableSource).toContain( - "openclaw/openclaw/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main", - ); - expect(reusableSource).toContain( - '--cert-identity "https://github.com/openclaw/openclaw/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main"', - ); - expect(reusableSource).toContain('--signer-digest "$CALLED_WORKFLOW_SHA"'); - const resolveJob = releaseWorkflow.jobs?.resolve_target; - expect(resolveJob?.outputs?.trusted_workflow_sha).toBeUndefined(); - expect( - resolveJob?.steps?.find((step) => step.name === "Resolve trusted main Telegram workflow SHA"), - ).toBeUndefined(); - const dispatchedWorkflow = parse(reusableSource) as { - on?: { - workflow_call?: { - inputs?: Record; - secrets?: Record; - }; - workflow_dispatch?: { - inputs?: Record; - }; - }; - }; - expect(dispatchedWorkflow.on?.workflow_dispatch?.inputs?.expected_trusted_workflow_sha).toEqual( - { - description: "Resolved main SHA authorized for this trusted workflow", - required: true, - type: "string", +function runCandidateProvenance(params: { openPr?: boolean; unsignedWebFlow?: boolean } = {}) { + const candidateSha = "a".repeat(40); + const workdir = tempDirs.make("openclaw-telegram-provenance-"); + const fakeBin = join(workdir, "bin"); + mkdirSync(fakeBin); + const metadata = { + data: { + repository: { + object: { + oid: candidateSha, + signature: params.unsignedWebFlow + ? null + : { isValid: true, state: "VALID", signer: { login: "release-maintainer" } }, + associatedPullRequests: { + nodes: [ + ...(params.openPr + ? [ + { + state: "OPEN", + headRefOid: candidateSha, + headRepository: { nameWithOwner: "openclaw/openclaw" }, + }, + ] + : []), + ...(params.unsignedWebFlow + ? [ + { + state: "MERGED", + baseRefName: "release/2026.7.1", + baseRepository: { nameWithOwner: "openclaw/openclaw" }, + mergeCommit: { oid: candidateSha }, + mergedBy: { login: "release-maintainer" }, + }, + ] + : []), + ], + }, + }, }, + }, + }; + writeFileSync( + join(fakeBin, "gh"), + `#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == *"api graphql"* ]]; then printf '%s\\n' "$FAKE_METADATA"; exit 0; fi +if [[ "$*" == *"/compare/"* ]]; then printf '%s\\n' "behind"; exit 0; fi +if [[ "$*" == *"/collaborators/release-maintainer/permission"* ]]; then printf '%s\\n' '{"permission":"write","role_name":"maintain"}'; exit 0; fi +exit 64 +`, + { mode: 0o755 }, + ); + writeFileSync( + join(fakeBin, "git"), + `#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == *"rev-parse HEAD"* ]]; then printf '%s\\n' "$TARGET_SHA"; exit 0; fi +if [[ "$*" == *"ls-remote"* ]]; then printf '%s\\trefs/heads/release/2026.7.1\\n' "$TARGET_SHA"; exit 0; fi +exit 64 +`, + { mode: 0o755 }, + ); + return spawnSync( + "bash", + ["-c", requireRun("build_candidate", "Validate candidate release provenance")], + { + cwd: workdir, + encoding: "utf8", + env: { + ...process.env, + FAKE_METADATA: JSON.stringify(metadata), + GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN: "HTTP 5[0-9][0-9]", + GITHUB_REPOSITORY: "openclaw/openclaw", + PATH: `${fakeBin}:${process.env.PATH}`, + TARGET_REF: "refs/heads/release/2026.7.1", + TARGET_SHA: candidateSha, + }, + }, + ); +} + +describe("release Telegram QA workflow", () => { + it("keeps the workflow wiring explicit and secret-scoped", () => { + const release = workflow(RELEASE_CHECKS_PATH); + const caller = release.jobs?.qa_live_telegram_release_checks; + expect(caller).toMatchObject({ + needs: ["resolve_target"], + permissions: { actions: "write", contents: "read" }, + "runs-on": "ubuntu-24.04", + "timeout-minutes": 210, + }); + expect(caller?.environment).toBeUndefined(); + expect(caller?.["continue-on-error"]).toBeUndefined(); + + const trusted = job("trusted_identity"); + expect(trusted).toMatchObject({ + permissions: { contents: "read", "id-token": "write" }, + "runs-on": "ubuntu-24.04", + "timeout-minutes": 5, + }); + expect(step("trusted_identity", "Verify dispatched-main identity").id).toBe("identity"); + + const runJob = job("run_telegram"); + expect(runJob.environment).toBe("qa-live-shared"); + expect(runJob["timeout-minutes"]).toBe(60); + expect(requireRun("advisory_status", "Record advisory status").trim()).toBe( + "set -euo pipefail\nnode scripts/release-telegram-qa.mjs advisory-status", ); - expect(dispatchedWorkflow.on?.workflow_dispatch?.inputs?.dispatch_id).toEqual({ - description: "Unique parent release-check dispatch identifier", - required: true, - type: "string", - }); - expect(dispatchedWorkflow.on?.workflow_call?.inputs?.expected_trusted_workflow_sha).toEqual({ - description: "Resolved main SHA authorized for this trusted workflow", - required: true, - type: "string", - }); - expect(dispatchedWorkflow.on?.workflow_call?.secrets).toHaveProperty( - "OPENCLAW_QA_CONVEX_SECRET_CI", - ); - }); - - it("bounds the OIDC identity request below the job timeout", () => { - const identityJob = workflowJob("trusted_identity"); - const identityStep = workflowStep(identityJob, "Verify dispatched-main identity"); - const oidcRequest = identityStep.run?.match( - /curl --fail --silent --show-error[\s\S]*?audience=openclaw-release-telegram-qa/u, - )?.[0]; - - expect(identityJob["timeout-minutes"]).toBe(5); - expect(oidcRequest).toContain("--connect-timeout 10"); - expect(oidcRequest).toContain("--max-time 30"); - }); - - it("binds dispatched and legacy reusable OIDC identity to the resolved main SHA", () => { - expect( - workflowStep(workflowJob("trusted_identity"), "Verify dispatched-main identity").env - ?.TARGET_REF, - ).toBe("${{ inputs.target_ref }}"); - - const trustedSha = "b".repeat(40); - const success = runIdentityVerification({ - expectedTrustedWorkflowSha: trustedSha, - }); - expect(success.status).toBe(0); - - const oidcDrifted = runIdentityVerification({ - expectedTrustedWorkflowSha: trustedSha, - oidcWorkflowSha: "c".repeat(40), - }); - expect(oidcDrifted.status).toBe(1); - expect(oidcDrifted.stderr).toContain("OIDC workflow_sha mismatch"); - - const mainMoved = runIdentityVerification({ - expectedTrustedWorkflowSha: trustedSha, - workflowSha: "c".repeat(40), - }); - expect(mainMoved.status).toBe(1); - expect(mainMoved.stderr).toBe(""); - - const reusableSuccess = runIdentityVerification({ - expectedTrustedWorkflowSha: trustedSha, - invocation: "reusable", - workflowSha: "d".repeat(40), - }); - expect(reusableSuccess.status).toBe(0); - - const reusableDrifted = runIdentityVerification({ - expectedTrustedWorkflowSha: trustedSha, - invocation: "reusable", - oidcJobWorkflowSha: "c".repeat(40), - workflowSha: "d".repeat(40), - }); - expect(reusableDrifted.status).toBe(1); - expect(reusableDrifted.stderr).toContain("OIDC job_workflow_sha mismatch"); - }); - - it("keeps candidate construction secretless and credentials inside the isolated runner", () => { - const source = readFileSync(WORKFLOW_PATH, "utf8"); - const workflow = parse(source) as { - jobs?: Record; - }; - const buildJob = workflow.jobs?.build_candidate; - const runJob = workflow.jobs?.run_telegram; - const buildRuntimeStep = buildJob?.steps?.find( - (step) => step.name === "Build candidate runtime without runner credentials", - ); - - expect(JSON.stringify(buildJob)).not.toContain("secrets."); - expect(buildRuntimeStep?.run).toContain("OPENCLAW_BUILD_PRIVATE_QA=1"); - expect(runJob?.environment).toBe("qa-live-shared"); - const secretSteps = runJob?.steps - ?.filter((step) => JSON.stringify(step).includes("secrets.")) - .map((step) => step.name); - expect(secretSteps).toEqual(["Validate required QA credential env", "Run Telegram live lane"]); - expect(source).not.toContain("secrets: inherit"); - expect(source).not.toContain("persist-credentials: true"); - expect(source).toContain("trusted_scenario_source=verified_trusted_workflow_sha"); - - for (const [jobName, job] of Object.entries(workflow.jobs ?? {})) { - for (const step of job.steps ?? []) { - if (step.uses?.startsWith("actions/checkout@")) { - expect(step.with?.["persist-credentials"], `${jobName}:${step.name}`).toBe(false); - } + for (const [jobName, value] of Object.entries(workflow().jobs ?? {})) { + for (const checkout of value.steps?.filter((candidate) => + candidate.uses?.startsWith("actions/checkout@"), + ) ?? []) { + expect(checkout.with?.["persist-credentials"], `${jobName}:${checkout.name}`).toBe(false); } } }); - it("resolves pnpm from the candidate package-manager pin", () => { - const buildJob = workflowJob("build_candidate"); - const installStep = workflowStep( - buildJob, - "Install candidate dependencies without runner credentials", - ); - const buildStep = workflowStep(buildJob, "Build candidate runtime without runner credentials"); - - expect(installStep["working-directory"]).toBe(".candidate"); - expect(installStep.run).toContain("pnpm install"); - expect(installStep.run).not.toContain("pnpm --dir .candidate"); - expect(buildStep["working-directory"]).toBe(".candidate"); - expect(buildStep.run).toContain("pnpm exec node scripts/build-all.mjs qaRuntime"); - expect(buildStep.run).not.toContain("pnpm --dir .candidate"); + it("accepts only the resolved trusted workflow identity", () => { + const trustedSha = "b".repeat(40); + expect(runIdentityVerification({ expectedTrustedWorkflowSha: trustedSha }).status).toBe(0); + expect( + runIdentityVerification({ + expectedTrustedWorkflowSha: trustedSha, + oidcWorkflowSha: "c".repeat(40), + }).stderr, + ).toContain("OIDC workflow_sha mismatch"); + expect( + runIdentityVerification({ + expectedTrustedWorkflowSha: trustedSha, + invocation: "reusable", + oidcJobWorkflowSha: "c".repeat(40), + }).stderr, + ).toContain("OIDC job_workflow_sha mismatch"); }); - it("allows the tracked-file index to exceed Node's default child-process buffer", () => { - const compareStep = workflowStep( - workflowJob("attest_candidate"), - "Compare candidate tracked source and tree", - ); + it("accepts trusted release provenance and rejects same-repository PR heads", () => { + const signed = runCandidateProvenance(); + expect(signed.status, signed.stderr).toBe(0); - expect(compareStep.run).toContain("maxBuffer: 16 * 1024 * 1024"); + const unsignedWebFlow = runCandidateProvenance({ unsignedWebFlow: true }); + expect(unsignedWebFlow.status, unsignedWebFlow.stderr).toBe(0); + + const openPr = runCandidateProvenance({ openPr: true }); + expect(openPr.status).toBe(1); + expect(openPr.stderr).toContain("open same-repository PR head"); }); - it("emits the release-check terminal status contract and fails closed", () => { - const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as { - jobs?: Record; - }; - const statusJob = workflow.jobs?.advisory_status; - const recordStep = statusJob?.steps?.find((step) => step.name === "Record advisory status"); - const uploadStep = statusJob?.steps?.find((step) => step.name === "Upload advisory status"); - const requireStep = statusJob?.steps?.find( - (step) => step.name === "Require successful Telegram release check", - ); - - for (const jobName of [ - "trusted_identity", - "build_candidate", - "attest_candidate", - "run_telegram", - ]) { - expect(workflow.jobs?.[jobName]?.["continue-on-error"], jobName).toBeUndefined(); - } - for (const jobName of ["build_candidate", "attest_candidate", "run_telegram"]) { - expect(workflow.jobs?.[jobName]?.if, jobName).toBe("always()"); - } - expect(statusJob?.if).toBe("always()"); - expect(recordStep?.run).toContain( - "qa_live_telegram_release_checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.env", - ); - for (const field of [ - "run_id", - "run_attempt", - "target_sha", - "workflow_sha", - "job", - "variant", - "status", - "job_status", - "step_outcomes", - ]) { - expect(recordStep?.run).toContain(`printf '${field}=`); - } - expect(uploadStep?.if).toBe("always()"); - expect(uploadStep?.with?.name).toBe( - "release-check-status-qa-live-telegram-${{ inputs.target_sha }}-${{ github.run_id }}-${{ github.run_attempt }}", - ); - expect(uploadStep?.with?.path).toContain("${{ steps.record_status.outputs.status_file }}"); - expect(uploadStep?.with?.path).toContain("${{ steps.record_status.outputs.evidence_file }}"); - expect(requireStep?.if).toBe("always()"); - expect(requireStep?.run).toContain('[[ "$STATUS" == "success" ]]'); - }); - - it("records producer failure and rejects the terminal status", () => { - const result = runAdvisoryStatus({ - BUILD_RESULT: "failure", - BUILD_STATUS: "failure", + it("writes terminal evidence only for complete successful producers", () => { + const success = runAdvisoryStatus(); + expect(success.result.status, success.result.stderr).toBe(0); + expect(success.outputs.status).toBe("success"); + expect(success.evidence).toMatchObject({ + kind: "release-check-status", + status: "success", + candidateArtifact: { id: "123", sourceSha: "a".repeat(40) }, }); - expect(result.recordResult.status).toBe(0); - expect(result.status).toBe("failure"); - expect(result.statusFile).toContain("status=failure\n"); - expect(result.statusFile).toContain("build:failure"); - expect(result.requireResult.status).toBe(1); + const failure = runAdvisoryStatus({ BUILD_RESULT: "failure", BUILD_STATUS: "failure" }); + expect(failure.result.status, failure.result.stderr).toBe(0); + expect(failure.outputs.status).toBe("failure"); + expect(failure.evidence.candidateArtifact).toMatchObject({ id: "123" }); + expect(failure.statusFile).toContain("build:failure"); }); - it("records empty producer output and rejects the terminal status", () => { - const result = runAdvisoryStatus({ - IDENTITY_STATUS: "", + it.runIf(process.platform === "linux")("retains only bounded, allowlisted diagnostics", () => { + const source = extractHereDocument( + requireRun("run_telegram", "Capture isolated Telegram runtime diagnostics"), + "NODE", + ); + const workdir = tempDirs.make("openclaw-telegram-diagnostics-"); + const scriptPath = join(workdir, "redact-gateway-tail.mts"); + const outputPath = join(workdir, "gateway.log"); + writeFileSync(scriptPath, source); + const records = [ + { 0: '{"subsystem":"gateway"}', 1: "ordinary info", _meta: { logLevelName: "INFO" } }, + { + 0: '{"subsystem":"agents/embedded"}', + 1: "embedded run start: safe", + _meta: { logLevelName: "DEBUG" }, + }, + { + 0: '{"subsystem":"agents/embedded"}', + 1: "private trace", + _meta: { logLevelName: "TRACE" }, + }, + ]; + const result = spawnSync(process.execPath, ["--import", "tsx", scriptPath, outputPath], { + cwd: process.cwd(), + encoding: "utf8", + input: `${records.map((record) => JSON.stringify(record)).join("\n")}\n`, }); - - expect(result.recordResult.status).toBe(0); - expect(result.status).toBe("failure"); - expect(result.statusFile).toContain("status=failure\n"); - expect(result.statusFile).toContain("identity: "); - expect(result.requireResult.status).toBe(1); + expect(result.status, result.stderr).toBe(0); + const output = readFileSync(outputPath, "utf8"); + expect(output).toContain("ordinary info"); + expect(output).toContain("embedded run start: safe"); + expect(output).not.toContain("private trace"); }); - it("accepts only complete successful producer evidence", () => { - const result = runAdvisoryStatus(); - - expect(result.recordResult.status).toBe(0); - expect(result.status).toBe("success"); - expect(result.statusFile).toContain("status=success\n"); - expect(result.requireResult.status).toBe(0); - }); - - it("keeps the isolated SUT lifetime below the credential lease TTL", () => { - const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as { - jobs?: Record; - }; - const job = workflow.jobs?.run_telegram; - expect(job?.["runs-on"]).toBe("ubuntu-24.04"); - expect(job?.["timeout-minutes"]).toBe(60); - - const validateStep = job?.steps?.find( - (step) => step.name === "Validate required QA credential env", - ); - expect(validateStep?.env?.RUNNER_ENVIRONMENT).toBe("${{ runner.environment }}"); - expect(validateStep?.env?.CREDENTIAL_ACQUIRE_TIMEOUT_MS).toBe("600000"); - expect(validateStep?.env?.JOB_TIMEOUT_MINUTES).toBe("60"); - expect(validateStep?.env?.LEASE_TTL_MS).toBe("7200000"); - expect(validateStep?.run).toContain('[[ "$RUNNER_ENVIRONMENT" == "github-hosted" ]]'); - expect(validateStep?.run).toContain("JOB_TIMEOUT_MINUTES * 60 * 1000 < LEASE_TTL_MS"); - - const runStep = job?.steps?.find((step) => step.name === "Run Telegram live lane"); - const createSutStep = job?.steps?.find( - (step) => step.name === "Create isolated Telegram SUT identity and launcher", - ); - expect(createSutStep?.run).toMatch( - /transport_keys=\([\s\S]*OPENCLAW_BUILD_PRIVATE_QA[\s\S]*OPENCLAW_ENABLE_PRIVATE_QA_CLI[\s\S]*\)/u, - ); - expect(runStep?.env?.OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS).toBe("600000"); - expect(runStep?.env?.OPENCLAW_QA_CREDENTIAL_LEASE_TTL_MS).toBe("7200000"); - expect(runStep?.env?.OPENCLAW_LOG_LEVEL).toBe("trace"); - expect(runStep?.env?.OPENCLAW_QA_TELEGRAM_SUT_CLEANUP_TIMEOUT_MS).toBe("60000"); - expect(runStep?.run).toContain("trap terminate_sut_uid_on_exit EXIT"); - expect(runStep?.run).toContain('"$OPENCLAW_QA_TELEGRAM_SUT_OPENCLAW_COMMAND" --terminate-uid'); - expect(runStep?.run).toContain("run_qa_attempt preflight --scenario channel-canary"); - expect(runStep?.run).toContain('candidate_telegram_qa="$CANDIDATE_ROOT/extensions/qa-lab'); - expect(runStep?.run).toContain("grep -Fq '\"openai/gpt-5.5\": {'"); - expect(runStep?.run).toContain("! grep -Fq '\"openai/gpt-5.6-luna\": {'"); - expect(runStep?.run).toContain('qa_model="mock-openai/gpt-5.5"'); - expect(runStep?.run).toContain('--model "$qa_model"'); - expect(runStep?.run).toContain("run_candidate_telegram_qa() ("); - expect(runStep?.run).toContain("exec node ./dist/index.js qa telegram"); - expect(runStep?.run).not.toContain('pnpm --dir "$CANDIDATE_ROOT" openclaw qa telegram'); - expect(runStep?.run).toContain( - "Telegram channel canary failed; skipping the remaining scenarios.", - ); - expect(runStep?.run).toContain("--list-scenarios"); - expect(runStep?.run).toContain('if [[ "$scenario_id" == "channel-canary" ]]; then'); - expect(runStep?.run).toContain("has_channel_canary=true"); - expect(runStep?.run).toContain("Candidate Telegram QA catalog has no default scenarios."); - expect(runStep?.run).toContain( - 'run_qa_attempt "attempt-${attempt}" "${remaining_scenarios[@]}"', - ); - expect(runStep?.run?.indexOf("--list-scenarios")).toBeLessThan( - runStep?.run?.indexOf("run_qa_attempt preflight --scenario channel-canary") ?? -1, - ); - expect( - runStep?.run?.indexOf("run_qa_attempt preflight --scenario channel-canary"), - ).toBeLessThan(runStep?.run?.indexOf("for attempt in 1 2") ?? -1); - - const finalizeStep = job?.steps?.find( - (step) => step.name === "Finalize trusted Telegram process-boundary evidence", - ); - expect(finalizeStep?.env?.RUN_LANE_OUTCOME).toBe("${{ steps.run_lane.outcome }}"); - expect(finalizeStep?.run).toContain('--arg runLaneOutcome "$RUN_LANE_OUTCOME"'); - expect(finalizeStep?.run).toContain('if $runLaneOutcome == "success" then 2 else 1 end'); - - const captureStep = job?.steps?.find( - (step) => step.name === "Capture isolated Telegram runtime diagnostics", - ); - expect(captureStep?.if).toContain("steps.terminate_sut.outputs.quiescent == 'true'"); - expect(captureStep?.env?.OUTPUT_DIR).toBe("${{ steps.run_lane.outputs.output_dir }}"); - expect(captureStep?.env?.RUNTIME_ROOT).toBe("${{ steps.create_sut.outputs.runtime_root }}"); - expect(captureStep?.env?.RUN_LANE_OUTCOME).toBe("${{ steps.run_lane.outcome }}"); - expect(captureStep?.run).toContain('[[ "$RUN_LANE_OUTCOME" != "success" ]]'); - expect(captureStep?.run).toContain("((${#gateway_logs[@]} > 0))"); - expect(captureStep?.run).toContain("mapfile -d '' -t gateway_logs"); - expect(captureStep?.run).toContain("-printf '%T@\\t%p\\0'"); - expect(captureStep?.run).toContain("sort -z -nr"); - expect(captureStep?.run).toContain("sed -z -n '1,8p'"); - expect(captureStep?.run).toContain("cut -z -f2-"); - expect(captureStep?.run).toContain("((${#gateway_logs[@]} <= 8))"); - expect(captureStep?.run).toContain("((${#model_config_proofs[@]} > 0))"); - expect(captureStep?.run).toContain("-name 'openclaw-*.log'"); - expect(captureStep?.run).toContain( - 'trusted_temp_root="$(mktemp -d "${RUNNER_TEMP}/openclaw-telegram-diagnostics.XXXXXX")"', - ); - expect(captureStep?.run).not.toContain(".raw"); - expect(captureStep?.run).toContain( - 'sudo cat "$log_path" | node --import tsx "$redactor_script" "$output_path"', - ); - expect(captureStep?.run).toContain("const redactedLine ="); - expect(captureStep?.run).toContain("const limitBytes = 131_072"); - expect(captureStep?.run).toContain("const maxInputRecordBytes = 1_048_576"); - expect(captureStep?.run).toContain("safeVerboseMessagePrefixes"); - expect(captureStep?.run).toContain("shouldRetainRecord"); - expect(captureStep?.run).toContain('"[trace:embedded-run] prep stages:"'); - expect(captureStep?.run).toContain('"[context-diag] pre-prompt:"'); - expect(captureStep?.run).toContain('"model.call.started"'); - expect(captureStep?.run).toContain("[truncated oversized gateway log record]"); - expect(captureStep?.run).toContain("[omitted oversized gateway log record]"); - expect(captureStep?.run).toContain("chunk.indexOf(0x0a, offset)"); - expect(captureStep?.run).not.toContain("readline.createInterface"); - expect(captureStep?.run).toContain("while (retained.length > 0"); - expect(captureStep?.run).toContain("redactQaGatewayDebugText"); - expect(captureStep?.run).toContain("model_config_proofs"); - expect(captureStep?.run).toContain("proof_bytes > 0 && proof_bytes <= 65536"); - expect(captureStep?.run).not.toContain("-name openclaw.json"); - expect( - job?.steps?.findIndex( - (step) => step.name === "Capture isolated Telegram runtime diagnostics", - ), - ).toBeLessThan( - job?.steps?.findIndex( - (step) => step.name === "Finalize trusted Telegram process-boundary evidence", - ) ?? -1, - ); - - const recordStep = job?.steps?.find((step) => step.name === "Record Telegram execution status"); - expect(recordStep?.env?.OUTCOMES).toContain("${{ steps.capture_diagnostics.outcome }}"); - }); - - it("serializes stderr behind the workflow-command pause", () => { - const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as { - jobs?: Record; - }; - const runStep = workflow.jobs?.run_telegram?.steps?.find( - (step) => step.name === "Run Telegram live lane", - ); - expect(runStep?.run).toMatch( - /run_qa_attempt\(\) \(\n\s+set -euo pipefail\n\s+exec 2>&1\n\s+output_name=/u, - ); - expect(runStep?.run).toContain("::stop-commands::%s"); - }); - - it.runIf(process.platform === "linux")("keeps only the newest eight gateway logs", () => { - const captureStep = workflowStep( - workflowJob("run_telegram"), - "Capture isolated Telegram runtime diagnostics", - ); - const selectorSource = captureStep.run?.match( - /mapfile -d '' -t gateway_logs < <\([\s\S]*?^\)$/mu, - )?.[0]; - expect(selectorSource).toBeTruthy(); - + it.runIf(process.platform === "linux")("keeps the newest eight gateway logs", () => { + const script = requireRun("run_telegram", "Capture isolated Telegram runtime diagnostics"); + const selector = script.match(/mapfile -d '' -t gateway_logs < <\([\s\S]*?^\)/mu)?.[0]; + expect(selector).toBeTruthy(); const workdir = tempDirs.make("openclaw-telegram-log-selector-"); const runtimeRoot = join(workdir, "runtime"); const fakeBin = join(workdir, "bin"); mkdirSync(join(runtimeRoot, "tmp"), { recursive: true }); mkdirSync(fakeBin); writeFileSync(join(fakeBin, "sudo"), '#!/bin/sh\nexec "$@"\n', { mode: 0o755 }); - - const logPaths = Array.from({ length: 12 }, (_, index) => { - const logDir = join(runtimeRoot, "tmp", `gateway-${index}`); - const logPath = join(logDir, `openclaw-${index}.log`); - mkdirSync(logDir); - writeFileSync(logPath, `${index}\n`); - utimesSync(logPath, index + 1, index + 1); - return logPath; + const paths = Array.from({ length: 12 }, (_, index) => { + const path = join(runtimeRoot, "tmp", `openclaw-${index}.log`); + writeFileSync(path, `${index}\n`); + utimesSync(path, index + 1, index + 1); + return path; }); const result = spawnSync( "bash", - ["-c", `set -euo pipefail\n${selectorSource}\nprintf '%s\\0' "\${gateway_logs[@]}"`], + ["-c", `set -euo pipefail\n${selector}\nprintf '%s\\0' "\${gateway_logs[@]}"`], { + encoding: "utf8", env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH}`, RUNTIME_ROOT: runtimeRoot }, }, ); - expect(result.status, result.stderr.toString()).toBe(0); - const selected = result.stdout.toString().split("\0").filter(Boolean); - expect(selected).toEqual(logPaths.slice(4).reverse()); - }); - - it("retains only allowlisted verbose runtime diagnostics", () => { - const captureStep = workflowStep( - workflowJob("run_telegram"), - "Capture isolated Telegram runtime diagnostics", - ); - const redactorSource = captureStep.run?.match( - /cat >"\$redactor_script" <<'NODE'\n([\s\S]*?)\nNODE/u, - )?.[1]; - expect(redactorSource).toBeTruthy(); - - const workdir = tempDirs.make("openclaw-telegram-log-filter-"); - const scriptPath = join(workdir, "redact-gateway-tail.mts"); - const outputPath = join(workdir, "gateway.log"); - writeFileSync(scriptPath, redactorSource ?? ""); - const inputRecords = [ - { 0: '{"subsystem":"gateway"}', 1: "ordinary info", _meta: { logLevelName: "INFO" } }, - { - 0: '{"subsystem":"agents/embedded"}', - 1: "embedded run start: safe milestone", - _meta: { logLevelName: "DEBUG" }, - }, - { - 0: '{"subsystem":"agents/embedded","details":"embedded run start: marker outside message"}', - 1: { details: "[context-diag] pre-prompt: structured marker outside message" }, - 2: "verbose payload must drop", - _meta: { logLevelName: "DEBUG" }, - }, - { - 0: '{"subsystem":"agents/embedded"}', - 1: "[context-diag] pre-prompt: safe counts", - _meta: { logLevelName: "TRACE" }, - }, - { - 0: '{"subsystem":"agents/embedded"}', - 1: "trace payload must drop", - message: "embedded run prompt end: convenience field must not authorize", - _meta: { logLevelName: "TRACE" }, - }, - ] - .map((record) => JSON.stringify(record)) - .join("\n"); - const input = `${inputRecords}\n{"0":"truncated verbose payload must drop"`; - const result = spawnSync(process.execPath, ["--import", "tsx", scriptPath, outputPath], { - cwd: process.cwd(), - encoding: "utf8", - input, - }); expect(result.status, result.stderr).toBe(0); - const output = readFileSync(outputPath, "utf8"); - expect(output).toContain("ordinary info"); - expect(output).toContain("embedded run start: safe milestone"); - expect(output).toContain("[context-diag] pre-prompt: safe counts"); - expect(output).not.toContain("verbose payload must drop"); - expect(output).not.toContain("marker outside message"); - expect(output).not.toContain("convenience field must not authorize"); - expect(output).not.toContain("truncated verbose payload must drop"); - expect(output).not.toContain("trace payload must drop"); + expect(result.stdout.toString().split("\0").filter(Boolean)).toEqual(paths.slice(4).reverse()); }); - it("derives SUT-writable paths from the verified runtime root after sudo", () => { - const source = readFileSync(WORKFLOW_PATH, "utf8"); - expect(source).toContain("Telegram SUT launcher failed: stage=%s line=%s status=%s"); - expect(source).toContain("launcher_stage=root-run-setup"); - expect(source).toContain("launcher_stage=enter-mount-namespace"); - expect(source).toContain("set_launcher_stage mask-host-paths"); - expect(source).toContain('launcher_stage_file="${RUNTIME_ROOT}/launcher-stage-${BASHPID}"'); - expect(source).toContain('set_launcher_stage "mask-host-path:${masked_path}"'); - expect(source).toContain("set_launcher_stage mount-proc"); - expect(source).toContain("set_launcher_stage write-identity"); - expect(source).toContain("set_launcher_stage launch-runtime"); - expect(source).toMatch(/set_launcher_stage launch-runtime\n\s+unset launcher_stage_file/u); - expect(source).toContain("Telegram SUT runtime preflight failed: stage=%s line=%s status=%s"); - expect(source).toContain("runtime_stage=verify-runtime-identity"); - expect(source).toContain("runtime_stage=verify-runtime-privileges"); - expect(source).toContain("runtime_stage=verify-parent-proc-hidden"); - expect(source).toContain("runtime_stage=verify-proc-visibility"); - expect(source).toContain("for _ in {1..100}; do"); - expect(source).toMatch( - /if tr "\\0" "\\n" <"\/proc\/\$\{control_pid\}\/environ" 2>\/dev\/null \|\n\s+grep -Fxq "OPENCLAW_QA_PROC_CONTROL=visible"; then/u, - ); - expect(source).toContain("proc_marker_visible=true\n break"); - expect(source).toContain("sleep 0.05"); - expect(source).toContain('[[ "$proc_marker_visible" == "true" ]]'); - expect(source).toMatch( - /kill "\$control_pid" >\/dev\/null 2>&1 \|\| true\n\s+wait "\$control_pid" \|\| true/u, - ); - expect(source).toContain("runtime_stage=verify-secret-env-hidden"); - expect(source).toContain("runtime_stage=verify-runner-fds-hidden"); - expect(source).toContain("runtime_stage=verify-runtime-files"); - expect(source).toContain("runtime_stage=verify-host-paths-hidden"); - expect(source).toContain("runtime_stage=sanitize-runtime-env"); - expect(source).toContain("runtime_stage=verify-runtime-env"); - expect(source).toContain("runtime_stage=write-sandbox-proof"); - expect(source).toContain("runtime_stage=exec-runtime"); - expect(source).toContain('TMPDIR="${SUT_RUNTIME_ROOT}/tmp"'); - expect(source).toContain('"$RUNTIME_ROOT"/tmp/openclaw-qa-suite-*'); - expect(source.indexOf("launcher_stage=enter-mount-namespace")).toBeLessThan( - source.indexOf("/usr/bin/unshare"), - ); - expect(source).not.toContain("exec /usr/bin/unshare"); - expect(source).not.toContain("set -x"); - expect(source).toContain('source_node_bin="$(realpath -e "$(command -v node)")"'); - expect(source).toContain('node_bin="${runtime_root}/node"'); - expect(source).toContain('sudo install -o root -g root -m 0555 "$source_node_bin" "$node_bin"'); - expect(source).toContain( - '[[ "$(stat -c \'%F:%a:%u:%g\' "$node_bin")" == "regular file:555:0:0" ]]', - ); - expect(source).toContain('"$node_bin" --version >/dev/null'); - expect(source).toContain('for masked_path in "$RUNNER_HOME" /tmp /var/tmp /dev/shm'); - expect(source).not.toMatch(/^\s+node_bin="\$\(realpath -e "\$\(command -v node\)"\)"$/mu); - expect(source).toContain('temp_root="$(realpath -e "${OPENCLAW_QA_TEMP_ROOT:?}")"'); - expect(source).toContain("sudo install -d -o root -g root -m 0700 /tmp/openclaw"); - expect(source).toContain('candidate_artifacts_dir="${CANDIDATE_ROOT}/.artifacts"'); - expect(source).toContain( - 'sudo install -d -o "$sut_uid" -g "$sut_gid" -m 0700 "$candidate_artifacts_dir"', - ); - expect(source).toContain( - '[[ "$(stat -c \'%F:%a:%u:%g\' "$candidate_artifacts_dir")" == "directory:700:${sut_uid}:${sut_gid}" ]]', - ); - expect(source).toContain("printf 'CANDIDATE_ARTIFACTS_DIR=%q\\n' \"$candidate_artifacts_dir\""); - expect(source).toContain("export CANDIDATE_ROOT CANDIDATE_ARTIFACTS_DIR RUNTIME_ROOT NODE_BIN"); - expect(source).toContain( - '[[ -d "${CANDIDATE_ARTIFACTS_DIR:?}" && -w "$CANDIDATE_ARTIFACTS_DIR" ]]', - ); - expect(source).toContain("CANDIDATE_ARTIFACTS_DIR \\"); - expect(source).toContain( - '-m 0711 \\\n "${runtime_root}/tmp/openclaw-${runner_uid}"', - ); - expect(source).toContain('"$RUNTIME_ROOT"/tmp/openclaw-"$RUNNER_UID"/openclaw-qa-suite-*'); - expect(source).toContain('proc_stat="$(cat "/proc/${pid}/stat")"'); - expect(source).not.toContain('proc_stat="$(cat /proc/self/stat)"'); - expect(source).toContain('if [[ "${1:-}" == "--root-verify" ]]'); - expect(source).toContain("signal.pidfd_send_signal(pidfd, signal_value)"); - expect(source).toContain('actual_executable="$(realpath -e "/proc/${pid}/exe")"'); - expect(source).toContain("cmdlineSha256: $cmdlineSha256"); - expect(source).toContain('export HOME="${temp_root}/home"'); - expect(source).toContain('export XDG_CONFIG_HOME="${temp_root}/xdg-config"'); - expect(source).toContain('if [[ "${1:-}" == "--root-terminate-uid" ]]'); - expect(source).toContain("OPENCLAW_LOG_LEVEL"); - expect(source).toContain("capture_live_model_config() {"); - expect(source).toContain('capture_live_model_config "$config_path"'); - expect(source).toContain('proof_tmp="${RUNTIME_ROOT}/gateway-model-config-${BASHPID}.json"'); - expect(source).toContain("proof_bytes > 0 && proof_bytes <= 65536"); - expect(source).toContain("before the QA suite removes its temp config"); - expect(source).toContain("agentDefaultModel:"); - expect(source).toContain("modelIds: ([.value.models[]?.id][:128]"); - }); - - it("keeps the generated SUT launcher valid bash", () => { - const createSutStep = workflowStep( - workflowJob("run_telegram"), + it("keeps generated SUT programs syntactically valid", () => { + const createSut = requireRun( + "run_telegram", "Create isolated Telegram SUT identity and launcher", ); - const launcherSource = createSutStep.run?.match( - /<<'LAUNCHER'\n([\s\S]*?)\nLAUNCHER(?:\n|$)/u, - )?.[1]; - expect(launcherSource).toBeTruthy(); - - const result = spawnSync("bash", ["-n"], { - encoding: "utf8", - input: launcherSource, - }); - expect(result.status, result.stderr).toBe(0); - }); - - it.runIf(process.platform === "linux")( - "captures only bounded model routing facts before candidate launch", - () => { - const createSutStep = workflowStep( - workflowJob("run_telegram"), - "Create isolated Telegram SUT identity and launcher", - ); - const launcherSource = createSutStep.run?.match( - /<<'LAUNCHER'\n([\s\S]*?)\nLAUNCHER(?:\n|$)/u, - )?.[1]; - const captureSource = launcherSource?.match( - /^capture_live_model_config\(\) \{[\s\S]*?^\}/mu, - )?.[0]; - expect(captureSource).toBeTruthy(); - - const workdir = tempDirs.make("openclaw-telegram-model-proof-"); - const runtimeRoot = join(workdir, "runtime"); - const evidenceRoot = join(workdir, "evidence"); - const configPath = join(workdir, "openclaw.json"); - const duplicateConfigPath = join(workdir, "openclaw-duplicate.json"); - mkdirSync(runtimeRoot); - mkdirSync(evidenceRoot); - writeFileSync( - configPath, - JSON.stringify({ - agents: { - defaults: { - model: { primary: "mock-openai/gpt-5.5", fallbacks: ["mock-openai/fallback"] }, - models: { "mock-openai/gpt-5.5": {}, "mock-openai/fallback": {} }, - }, - }, - models: { - providers: { - "mock-openai": { - api: "openai-responses", - endpoint: "https://example.invalid", - ignoredField: "not-exported", - models: [{ id: "gpt-5.5", ignoredField: "not-exported" }], - }, - }, - }, - }), - ); - writeFileSync( - duplicateConfigPath, - readFileSync(configPath, "utf8").replace("not-exported", "different-ignored-value"), - ); - const result = spawnSync( - "bash", - [ - "-c", - `set -euo pipefail\n${captureSource}\ncapture_live_model_config "$CONFIG_PATH"\ncapture_live_model_config "$DUPLICATE_CONFIG_PATH"\nfind "$EVIDENCE_ROOT/trusted-runtime-diagnostics" -type f -name 'gateway-model-config-*.json' -print`, - ], - { - encoding: "utf8", - env: { - ...process.env, - CONFIG_PATH: configPath, - DUPLICATE_CONFIG_PATH: duplicateConfigPath, - EVIDENCE_ROOT: evidenceRoot, - RUNTIME_ROOT: runtimeRoot, - RUNNER_GID: String(process.getgid?.() ?? 0), - RUNNER_UID: String(process.getuid?.() ?? 0), - }, - }, - ); - expect(result.status, result.stderr).toBe(0); - const proofPaths = result.stdout.trim().split("\n"); - expect(proofPaths).toHaveLength(1); - const proofPath = proofPaths[0]!; - const proofText = readFileSync(proofPath, "utf8"); - expect(JSON.parse(proofText)).toEqual({ - agentDefaultModel: { - primary: "mock-openai/gpt-5.5", - fallbacks: ["mock-openai/fallback"], - }, - agentModelRefs: ["mock-openai/fallback", "mock-openai/gpt-5.5"], - providers: [{ id: "mock-openai", api: "openai-responses", modelIds: ["gpt-5.5"] }], - }); - expect(proofText).not.toContain("not-exported"); - expect(proofText).not.toContain("example.invalid"); - }, - ); - - it("arms the boundary preload only in the gateway main thread", () => { - const createSutStep = workflowStep( - workflowJob("run_telegram"), - "Create isolated Telegram SUT identity and launcher", - ); - const preloadSource = createSutStep.run?.match(/<<'PRELOAD'\n([\s\S]*?)\nPRELOAD/u)?.[1]; - expect(preloadSource).toBeTruthy(); - + const launcher = extractHereDocument(createSut, "LAUNCHER"); + expect(spawnSync("bash", ["-n"], { encoding: "utf8", input: launcher }).status).toBe(0); + const preload = extractHereDocument(createSut, "PRELOAD"); const workdir = tempDirs.make("openclaw-telegram-preload-"); const preloadPath = join(workdir, "preload.mjs"); - writeFileSync(preloadPath, preloadSource ?? ""); + writeFileSync(preloadPath, preload); const env = { ...process.env }; delete env.OPENCLAW_QA_SUT_PREENTRY_STOP; - - const mainResult = spawnSync(process.execPath, ["--import", preloadPath, "-e", ""], { - encoding: "utf8", - env, - }); - expect(mainResult.status).not.toBe(0); - expect(mainResult.stderr).toContain("trusted Telegram SUT preload was not armed"); - - const workerResult = spawnSync( - process.execPath, - [ - "-e", - `const { Worker } = require("node:worker_threads");\n` + - `const worker = new Worker('require("node:worker_threads").parentPort.postMessage("ready")', { eval: true, execArgv: ["--import", ${JSON.stringify(preloadPath)}] });\n` + - `worker.once("message", () => process.exit(0));\n` + - `worker.once("error", (error) => { console.error(error); process.exit(1); });`, - ], - { encoding: "utf8", env, killSignal: "SIGKILL", timeout: 5_000 }, - ); - expect(workerResult.status, workerResult.stderr).toBe(0); - }); - - it("reports the namespace-entry stage when its supervised child fails", () => { - const source = readFileSync(WORKFLOW_PATH, "utf8"); - const trapLine = source.match(/^\s+(trap 'exit_status=.*' ERR)$/mu)?.[1]; - expect(trapLine).toBeTruthy(); - - const result = spawnSync( - "bash", - [ - "-c", - `set -Eeuo pipefail\nlauncher_stage=enter-mount-namespace\n${trapLine}\nbash -c 'exit 23'`, - ], - { encoding: "utf8" }, - ); - - expect(result.status).toBe(23); - expect(result.stderr).toMatch( - /Telegram SUT launcher failed: stage=enter-mount-namespace line=[0-9]+ status=23/u, - ); - }); - - it("reports the persisted inner launcher stage when namespace supervision fails", () => { - const source = readFileSync(WORKFLOW_PATH, "utf8"); - const trapLine = source.match(/^\s+(trap 'exit_status=.*' ERR)$/mu)?.[1]; - expect(trapLine).toBeTruthy(); - - const workdir = tempDirs.make("openclaw-telegram-launcher-stage-"); - const stagePath = join(workdir, "stage"); - writeFileSync(stagePath, "mount-proc\n"); - const result = spawnSync( - "bash", - [ - "-c", - `set -Eeuo pipefail\nlauncher_stage=enter-mount-namespace\nlauncher_stage_file=${JSON.stringify(stagePath)}\n${trapLine}\nbash -c 'exit 23'`, - ], - { encoding: "utf8" }, - ); - - expect(result.status).toBe(23); - expect(result.stderr).toMatch( - /Telegram SUT launcher failed: stage=mount-proc line=[0-9]+ status=23/u, - ); - }); - - it("reports the exact failing runtime preflight line", () => { - const source = readFileSync(WORKFLOW_PATH, "utf8"); - const diagnosticSource = source.match( - /^\s+(fail_runtime_stage\(\) \{[\s\S]*?^\s+\}\n\s+trap "fail_runtime_stage \\?\$\? \\?\$LINENO" ERR)$/mu, - )?.[1]; - expect(diagnosticSource).toBeTruthy(); - - const script = `set -Eeuo pipefail\nruntime_stage=runtime-test\n${diagnosticSource}\nfalse`; - const failureLine = script.split("\n").findIndex((line) => line === "false") + 1; - const result = spawnSync("bash", ["-c", script], { encoding: "utf8" }); - - expect(result.status).toBe(1); - expect(result.stderr).toContain( - `Telegram SUT runtime preflight failed: stage=runtime-test line=${failureLine} status=1`, - ); + expect( + spawnSync(process.execPath, ["--import", preloadPath, "-e", ""], { encoding: "utf8", env }) + .status, + ).not.toBe(0); }); }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 36c23d080ba9..4973d548a4d3 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -3417,17 +3417,9 @@ describe("package artifact reuse", () => { const telegramStatus = workflowJob(RELEASE_TELEGRAM_QA_WORKFLOW, "advisory_status"); expect(telegramStatus["continue-on-error"]).toBeUndefined(); const telegramRecord = workflowStep(telegramStatus, "Record advisory status"); - expectTextToIncludeAll(telegramRecord.run, [ - "run_id=", - "run_attempt=", - "target_sha=", - "workflow_sha=", - "job=", - "variant=", - "status=", - "job_status=", - "step_outcomes=", - ]); + expect(telegramRecord.run?.trim()).toBe( + "set -euo pipefail\nnode scripts/release-telegram-qa.mjs advisory-status", + ); const telegramStatusUpload = workflowStep(telegramStatus, "Upload advisory status"); expect(telegramStatusUpload.if).toBe("always()"); expect(telegramStatusUpload.uses).toBe(UPLOAD_ARTIFACT_V7);