diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index 64d4a5b27217..775566f29aba 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -894,9 +894,8 @@ jobs: if [[ "$child_rerun_group" == "release-checks" ]]; then child_rerun_group=all fi - release_checks_target_ref="${TARGET_CONTEXT_REF:-$TARGET_REF}" args=( - -f ref="$release_checks_target_ref" + -f ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f provider="$PROVIDER" -f mode="$MODE" @@ -908,6 +907,7 @@ jobs: -f rerun_group="$child_rerun_group" ) if [[ -n "${TARGET_CONTEXT_REF// }" ]]; then + args+=(-f target_context_ref="$TARGET_CONTEXT_REF") args+=(-f allow_frozen_target_scenario_omissions=true) fi if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index bc105f925d67..4c5f6df5a015 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -9,6 +9,11 @@ on: description: Branch, tag, or full commit SHA to validate required: true type: string + target_context_ref: + description: Optional canonical release branch or tag context for an exact-SHA target + required: false + default: "" + type: string expected_sha: description: Optional full SHA that ref must resolve to required: false @@ -1974,7 +1979,7 @@ jobs: - name: Dispatch and await trusted Telegram QA env: GH_TOKEN: ${{ github.token }} - TARGET_REF: ${{ needs.resolve_target.outputs.ref }} + TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} shell: bash run: | @@ -1983,13 +1988,6 @@ jobs: workflow="openclaw-release-telegram-qa.yml" run_id="" run_name="" - telegram_target_ref="$TARGET_REF" - case "$telegram_target_ref" in - validation/target-* | refs/heads/validation/target-*) - telegram_target_ref="$TARGET_SHA" - ;; - esac - find_child_runs() { RUN_NAME="$run_name" gh api -X GET \ "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \ @@ -2041,7 +2039,8 @@ jobs: --ref main \ -f dispatch_id="$dispatch_id" \ -f expected_trusted_workflow_sha="$expected_trusted_workflow_sha" \ - -f target_ref="$telegram_target_ref" \ + -f target_context_ref="$TARGET_CONTEXT_REF" \ + -f target_ref="$TARGET_SHA" \ -f target_sha="$TARGET_SHA" for _ in $(seq 1 60); do diff --git a/.github/workflows/openclaw-release-telegram-qa.yml b/.github/workflows/openclaw-release-telegram-qa.yml index 1fd95cb061bc..4d4f56acff1f 100644 --- a/.github/workflows/openclaw-release-telegram-qa.yml +++ b/.github/workflows/openclaw-release-telegram-qa.yml @@ -15,6 +15,11 @@ on: description: Trusted release ref whose exact candidate SHA should be validated required: true type: string + target_context_ref: + description: Optional canonical release branch or tag context for an exact-SHA candidate + required: false + default: "" + type: string target_sha: description: Exact full candidate commit SHA required: true @@ -40,6 +45,11 @@ on: description: Trusted release ref whose exact candidate SHA should be validated required: true type: string + target_context_ref: + description: Optional canonical release branch or tag context for an exact-SHA candidate + required: false + default: "" + type: string target_sha: description: Exact full candidate commit SHA required: true @@ -73,6 +83,7 @@ jobs: CALLER_WORKFLOW_SHA: ${{ github.workflow_sha }} EXPECTED_TRUSTED_WORKFLOW_SHA: ${{ inputs.expected_trusted_workflow_sha }} JOB_CONTEXT: ${{ toJSON(job) }} + TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} TARGET_REF: ${{ inputs.target_ref }} TARGET_SHA: ${{ inputs.target_sha }} WORKFLOW_REF: ${{ github.workflow_ref }} @@ -90,6 +101,18 @@ jobs: echo "Telegram QA identity or target is malformed." >&2 exit 1 fi + normalized_context_ref="${TARGET_CONTEXT_REF:-}" + normalized_context_ref="${normalized_context_ref#refs/heads/}" + normalized_context_ref="${normalized_context_ref#refs/tags/}" + if [[ -n "$normalized_context_ref" && + ! "$normalized_context_ref" =~ ^(release/[0-9]{4}\.[0-9]+\.[0-9]+|extended-stable/[0-9]{4}\.[0-9]+\.33|v[0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?)$ ]]; then + echo "Telegram QA target context must be a canonical release branch or tag." >&2 + exit 1 + fi + if [[ -n "$normalized_context_ref" && "$TARGET_REF" != "$TARGET_SHA" ]]; then + echo "Telegram QA release context requires an exact-SHA target ref." >&2 + exit 1 + fi INVOCATION_MODE=reusable if [[ "$WORKFLOW_REF" == "$expected_ref" ]]; then INVOCATION_MODE=dispatch @@ -262,6 +285,7 @@ jobs: id: provenance env: GH_TOKEN: ${{ github.token }} + TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} TARGET_REF: ${{ inputs.target_ref }} TARGET_SHA: ${{ inputs.target_sha }} shell: bash @@ -304,6 +328,42 @@ jobs: candidate_sha="$(git -C .candidate rev-parse HEAD)" [[ "$candidate_sha" == "$TARGET_SHA" ]] + normalized_context_ref="${TARGET_CONTEXT_REF:-}" + normalized_context_ref="${normalized_context_ref#refs/heads/}" + normalized_context_ref="${normalized_context_ref#refs/tags/}" + context_release_branch="" + context_release_tag="" + frozen_release_branch_pattern="" + if [[ "$normalized_context_ref" =~ ^release/([0-9]{4}\.[0-9]+\.[0-9]+)$ ]]; then + release_version="${BASH_REMATCH[1]}" + release_version_pattern="${release_version//./\\.}" + candidate_version="$(jq -er '.version' .candidate/package.json)" + if [[ "$candidate_version" == "$release_version" ]]; then + context_release_branch="$normalized_context_ref" + elif [[ "$candidate_version" =~ ^${release_version_pattern}-beta\.[0-9]+$ ]]; then + candidate_version_pattern="${candidate_version//./\\.}" + frozen_release_branch_pattern="^release/${candidate_version_pattern}-code-frozen(-r[1-9][0-9]*)?$" + else + echo "Telegram candidate version ${candidate_version} does not belong to release ${release_version}." >&2 + exit 1 + fi + elif [[ "$normalized_context_ref" =~ ^extended-stable/([0-9]{4}\.[0-9]+\.33)$ ]]; then + context_version="${BASH_REMATCH[1]}" + candidate_version="$(jq -er '.version' .candidate/package.json)" + if [[ "$candidate_version" != "$context_version" ]]; then + echo "Telegram candidate version ${candidate_version} does not match context ${normalized_context_ref}." >&2 + exit 1 + fi + context_release_branch="$normalized_context_ref" + elif [[ "$normalized_context_ref" =~ ^v([0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?)$ ]]; then + context_version="${BASH_REMATCH[1]}" + candidate_version="$(jq -er '.version' .candidate/package.json)" + if [[ "$candidate_version" != "$context_version" ]]; then + echo "Telegram candidate version ${candidate_version} does not match context ${normalized_context_ref}." >&2 + exit 1 + fi + context_release_tag="$normalized_context_ref" + fi repository_owner="${GITHUB_REPOSITORY%%/*}" repository_name="${GITHUB_REPOSITORY#*/}" candidate_metadata_json="$( @@ -334,18 +394,41 @@ jobs: )" trusted_reason="" trusted_release_branch="" - if [[ "$compare_status" == "ahead" || "$compare_status" == "identical" ]]; then + if [[ -n "$context_release_branch" ]]; then + branch_sha="$( + git -C .candidate ls-remote --exit-code --refs origin \ + "refs/heads/${context_release_branch}" | + awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }' + )" + [[ "$branch_sha" == "$candidate_sha" ]] + trusted_reason="release-branch-head" + trusted_release_branch="$context_release_branch" + elif [[ -n "$context_release_tag" ]]; then + tag_refs="$( + git -C .candidate ls-remote --exit-code origin \ + "refs/tags/${context_release_tag}" "refs/tags/${context_release_tag}^{}" + )" + awk -v sha="$candidate_sha" '$1 == sha { found = 1 } END { exit(found ? 0 : 1) }' \ + <<<"$tag_refs" + trusted_reason="release-tag" + elif [[ -z "$frozen_release_branch_pattern" && + ( "$compare_status" == "ahead" || "$compare_status" == "identical" ) ]]; then trusted_reason="main-ancestor" else normalized_ref="${TARGET_REF#refs/heads/}" - if [[ "$normalized_ref" =~ ^(release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*|extended-stable/[0-9]{4}\.[1-9][0-9]*\.33)$ ]]; then + if [[ "$normalized_ref" =~ ^(release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*|extended-stable/[0-9]{4}\.[1-9][0-9]*\.33)$ ]] || + [[ -n "$frozen_release_branch_pattern" && "$normalized_ref" =~ $frozen_release_branch_pattern ]]; then branch_sha="$( git -C .candidate ls-remote --exit-code --refs origin \ "refs/heads/${normalized_ref}" | awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }' )" [[ "$branch_sha" == "$candidate_sha" ]] - trusted_reason="release-branch-head" + if [[ -n "$frozen_release_branch_pattern" && "$normalized_ref" =~ $frozen_release_branch_pattern ]]; then + trusted_reason="frozen-release-branch-head" + else + trusted_reason="release-branch-head" + fi trusted_release_branch="$normalized_ref" elif [[ "$TARGET_REF" =~ ^refs/tags/v ]] || [[ "$TARGET_REF" =~ ^v ]]; then normalized_tag="${TARGET_REF#refs/tags/}" @@ -361,13 +444,20 @@ jobs: gh_with_retry api --paginate \ "repos/${GITHUB_REPOSITORY}/commits/${candidate_sha}/branches-where-head" \ --jq '.[].name' | - awk '$0 ~ /^release\/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$/ || - $0 ~ /^extended-stable\/[0-9]{4}\.[1-9][0-9]*\.33$/ { print }' + awk -v frozen="$frozen_release_branch_pattern" \ + '(frozen != "" && $0 ~ frozen) || + (frozen == "" && + ($0 ~ /^release\/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$/ || + $0 ~ /^extended-stable\/[0-9]{4}\.[1-9][0-9]*\.33$/)) { print }' )" if [[ "$(wc -l <<<"$matching_release_branches" | tr -d ' ')" == "1" && -n "$matching_release_branches" ]]; then - trusted_reason="release-branch-head" + if [[ -n "$frozen_release_branch_pattern" && "$matching_release_branches" =~ $frozen_release_branch_pattern ]]; then + trusted_reason="frozen-release-branch-head" + else + trusted_reason="release-branch-head" + fi trusted_release_branch="$matching_release_branches" - else + elif [[ -z "$frozen_release_branch_pattern" ]]; then matching_release_tags="$( git -C .candidate ls-remote origin 'refs/tags/v*' | awk -v sha="$candidate_sha" '$1 == sha { sub(/\^\{\}$/, "", $2); print $2 }' | @@ -403,6 +493,11 @@ jobs: exit 1 fi signer="$(jq -r '.data.repository.object.signature.signer.login // ""' <<<"$signature_json")" + if [[ "$trusted_reason" == "frozen-release-branch-head" && + ( "$signature_status" != "valid" || "$signer" == "web-flow" ) ]]; then + echo "Frozen release candidate ${candidate_sha} requires a valid maintainer signature." >&2 + exit 1 + fi permission_actor="$signer" if [[ "$signature_status" == "missing" || "$signer" == "web-flow" ]]; then if [[ "$trusted_reason" != "release-branch-head" || -z "$trusted_release_branch" ]]; then @@ -1004,7 +1099,9 @@ jobs: - name: Revalidate candidate release provenance id: revalidate_provenance env: + CANDIDATE_ROOT: ${{ steps.extract_candidate.outputs.candidate_root }} GH_TOKEN: ${{ github.token }} + TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} TARGET_REF: ${{ inputs.target_ref }} TARGET_SHA: ${{ inputs.target_sha }} shell: bash @@ -1046,6 +1143,42 @@ jobs: } candidate_sha="$TARGET_SHA" + normalized_context_ref="${TARGET_CONTEXT_REF:-}" + normalized_context_ref="${normalized_context_ref#refs/heads/}" + normalized_context_ref="${normalized_context_ref#refs/tags/}" + context_release_branch="" + context_release_tag="" + frozen_release_branch_pattern="" + if [[ "$normalized_context_ref" =~ ^release/([0-9]{4}\.[0-9]+\.[0-9]+)$ ]]; then + release_version="${BASH_REMATCH[1]}" + release_version_pattern="${release_version//./\\.}" + candidate_version="$(jq -er '.version' "${CANDIDATE_ROOT}/package.json")" + if [[ "$candidate_version" == "$release_version" ]]; then + context_release_branch="$normalized_context_ref" + elif [[ "$candidate_version" =~ ^${release_version_pattern}-beta\.[0-9]+$ ]]; then + candidate_version_pattern="${candidate_version//./\\.}" + frozen_release_branch_pattern="^release/${candidate_version_pattern}-code-frozen(-r[1-9][0-9]*)?$" + else + echo "Telegram candidate version ${candidate_version} does not belong to release ${release_version}." >&2 + exit 1 + fi + elif [[ "$normalized_context_ref" =~ ^extended-stable/([0-9]{4}\.[0-9]+\.33)$ ]]; then + context_version="${BASH_REMATCH[1]}" + candidate_version="$(jq -er '.version' "${CANDIDATE_ROOT}/package.json")" + if [[ "$candidate_version" != "$context_version" ]]; then + echo "Telegram candidate version ${candidate_version} does not match context ${normalized_context_ref}." >&2 + exit 1 + fi + context_release_branch="$normalized_context_ref" + elif [[ "$normalized_context_ref" =~ ^v([0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?)$ ]]; then + context_version="${BASH_REMATCH[1]}" + candidate_version="$(jq -er '.version' "${CANDIDATE_ROOT}/package.json")" + if [[ "$candidate_version" != "$context_version" ]]; then + echo "Telegram candidate version ${candidate_version} does not match context ${normalized_context_ref}." >&2 + exit 1 + fi + context_release_tag="$normalized_context_ref" + fi repository_owner="${GITHUB_REPOSITORY%%/*}" repository_name="${GITHUB_REPOSITORY#*/}" candidate_metadata_json="$( @@ -1073,17 +1206,39 @@ jobs: )" trusted_reason="" trusted_release_branch="" - if [[ "$compare_status" == "ahead" || "$compare_status" == "identical" ]]; then + if [[ -n "$context_release_branch" ]]; then + branch_sha="$( + git ls-remote --exit-code --refs origin "refs/heads/${context_release_branch}" | + awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }' + )" + [[ "$branch_sha" == "$candidate_sha" ]] + trusted_reason="release-branch-head" + trusted_release_branch="$context_release_branch" + elif [[ -n "$context_release_tag" ]]; then + tag_refs="$( + git ls-remote --exit-code origin \ + "refs/tags/${context_release_tag}" "refs/tags/${context_release_tag}^{}" + )" + awk -v sha="$candidate_sha" '$1 == sha { found = 1 } END { exit(found ? 0 : 1) }' \ + <<<"$tag_refs" + trusted_reason="release-tag" + elif [[ -z "$frozen_release_branch_pattern" && + ( "$compare_status" == "ahead" || "$compare_status" == "identical" ) ]]; then trusted_reason="main-ancestor" else normalized_ref="${TARGET_REF#refs/heads/}" - if [[ "$normalized_ref" =~ ^(release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*|extended-stable/[0-9]{4}\.[1-9][0-9]*\.33)$ ]]; then + if [[ "$normalized_ref" =~ ^(release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*|extended-stable/[0-9]{4}\.[1-9][0-9]*\.33)$ ]] || + [[ -n "$frozen_release_branch_pattern" && "$normalized_ref" =~ $frozen_release_branch_pattern ]]; then branch_sha="$( git ls-remote --exit-code --refs origin "refs/heads/${normalized_ref}" | awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }' )" [[ "$branch_sha" == "$candidate_sha" ]] - trusted_reason="release-branch-head" + if [[ -n "$frozen_release_branch_pattern" && "$normalized_ref" =~ $frozen_release_branch_pattern ]]; then + trusted_reason="frozen-release-branch-head" + else + trusted_reason="release-branch-head" + fi trusted_release_branch="$normalized_ref" elif [[ "$TARGET_REF" =~ ^refs/tags/v ]] || [[ "$TARGET_REF" =~ ^v ]]; then normalized_tag="${TARGET_REF#refs/tags/}" @@ -1099,13 +1254,20 @@ jobs: gh_with_retry api --paginate \ "repos/${GITHUB_REPOSITORY}/commits/${candidate_sha}/branches-where-head" \ --jq '.[].name' | - awk '$0 ~ /^release\/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$/ || - $0 ~ /^extended-stable\/[0-9]{4}\.[1-9][0-9]*\.33$/ { print }' + awk -v frozen="$frozen_release_branch_pattern" \ + '(frozen != "" && $0 ~ frozen) || + (frozen == "" && + ($0 ~ /^release\/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$/ || + $0 ~ /^extended-stable\/[0-9]{4}\.[1-9][0-9]*\.33$/)) { print }' )" if [[ "$(wc -l <<<"$matching_release_branches" | tr -d ' ')" == "1" && -n "$matching_release_branches" ]]; then - trusted_reason="release-branch-head" + if [[ -n "$frozen_release_branch_pattern" && "$matching_release_branches" =~ $frozen_release_branch_pattern ]]; then + trusted_reason="frozen-release-branch-head" + else + trusted_reason="release-branch-head" + fi trusted_release_branch="$matching_release_branches" - else + elif [[ -z "$frozen_release_branch_pattern" ]]; then matching_release_tags="$( git ls-remote origin 'refs/tags/v*' | awk -v sha="$candidate_sha" '$1 == sha { sub(/\^\{\}$/, "", $2); print $2 }' | @@ -1138,6 +1300,11 @@ jobs: exit 1 fi signer="$(jq -r '.data.repository.object.signature.signer.login // ""' <<<"$signature_json")" + if [[ "$trusted_reason" == "frozen-release-branch-head" && + ( "$signature_status" != "valid" || "$signer" == "web-flow" ) ]]; then + echo "Frozen release candidate ${candidate_sha} requires a valid maintainer signature." >&2 + exit 1 + fi permission_actor="$signer" if [[ "$signature_status" == "missing" || "$signer" == "web-flow" ]]; then if [[ "$trusted_reason" != "release-branch-head" || -z "$trusted_release_branch" ]]; then diff --git a/scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs b/scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs index 4053a7064d05..e8c1d9007d39 100644 --- a/scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs +++ b/scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs @@ -905,10 +905,12 @@ function assertAgentError() { ) : ""; const combined = `${stdout}\n${stderr}`; - if ( - !combined.includes('Requested agent harness "codex" is not registered') && - !combined.includes("Unknown model: codex/") - ) { + const expectedErrors = [ + 'Requested agent harness "codex" is not registered', + "Unknown model: codex/", + 'Agent harness runtime "codex" is not present in the prepared registry.', + ]; + if (!expectedErrors.some((message) => combined.includes(message))) { throw new Error(`unexpected post-uninstall agent error:\nstdout=${stdout}\nstderr=${stderr}`); } } diff --git a/scripts/e2e/lib/upgrade-survivor/run.sh b/scripts/e2e/lib/upgrade-survivor/run.sh index 6e43fb994b12..1d6aedc6f99e 100644 --- a/scripts/e2e/lib/upgrade-survivor/run.sh +++ b/scripts/e2e/lib/upgrade-survivor/run.sh @@ -434,7 +434,7 @@ restore_prepublish_authored_config() { configure_plugin_registry() { local fixture_root="$ARTIFACT_ROOT/plugin-registry" local package_dir="$fixture_root/package" - local tarball="$fixture_root/openclaw-brave-plugin-2026.5.2.tgz" + local tarball="$fixture_root/openclaw-brave-plugin-${candidate_version}.tgz" local port_file="$fixture_root/npm-registry-port" local log_file="$fixture_root/npm-registry.log" local registry_args=() @@ -473,17 +473,21 @@ NODE if configured_plugin_installs_enabled; then mkdir -p "$package_dir" - FIXTURE_PACKAGE_DIR="$package_dir" node <<'NODE' + FIXTURE_PACKAGE_DIR="$package_dir" FIXTURE_PACKAGE_VERSION="$candidate_version" node <<'NODE' const fs = require("node:fs"); const path = require("node:path"); const root = process.env.FIXTURE_PACKAGE_DIR; +const version = process.env.FIXTURE_PACKAGE_VERSION; +if (!version) { + throw new Error("missing fixture package version"); +} fs.mkdirSync(root, { recursive: true }); fs.writeFileSync( path.join(root, "package.json"), `${JSON.stringify( { name: "@openclaw/brave-plugin", - version: "2026.5.2", + version, openclaw: { extensions: ["./index.js"] }, }, null, @@ -524,7 +528,7 @@ fs.writeFileSync( ); NODE tar -czf "$tarball" -C "$fixture_root" package - registry_args+=("@openclaw/brave-plugin" "2026.5.2" "$tarball") + registry_args+=("@openclaw/brave-plugin" "$candidate_version" "$tarball") fi if [ "${#registry_args[@]}" -eq 0 ]; then diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 1aa62ecbf9de..31e02762d3d1 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -7085,19 +7085,84 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" }, ); - it("pins transient validation refs before trusted Telegram QA dispatch", () => { + it("keeps exact release validation identity separate from release context", () => { + const fullReleaseWorkflow = readWorkflow(".github/workflows/full-release-validation.yml"); const releaseWorkflow = readReleaseChecksWorkflow(); + const telegramWorkflow = readWorkflow(".github/workflows/openclaw-release-telegram-qa.yml"); + const fullReleaseDispatchStep = fullReleaseWorkflow.jobs.release_checks.steps.find( + (step: WorkflowStep) => step.name === "Dispatch and monitor release checks", + ); const dispatchStep = releaseWorkflow.jobs.qa_live_telegram_release_checks.steps.find( (step: WorkflowStep) => step.name === "Dispatch and await trusted Telegram QA", ); + const identityStep = telegramWorkflow.jobs.trusted_identity.steps.find( + (step: WorkflowStep) => step.name === "Verify dispatched-main identity", + ); + const provenanceSteps = [ + telegramWorkflow.jobs.build_candidate.steps.find( + (step: WorkflowStep) => step.name === "Validate candidate release provenance", + ), + telegramWorkflow.jobs.run_telegram.steps.find( + (step: WorkflowStep) => step.name === "Revalidate candidate release provenance", + ), + ]; - expect(dispatchStep.env.TARGET_REF).toBe("${{ needs.resolve_target.outputs.ref }}"); + expect(fullReleaseWorkflow.on.workflow_dispatch.inputs.target_context_ref).toMatchObject({ + required: false, + default: "", + type: "string", + }); + expect(fullReleaseDispatchStep.run).toContain('-f ref="$TARGET_SHA"'); + expect(fullReleaseDispatchStep.run).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"'); + expect(fullReleaseDispatchStep.run).not.toContain( + 'release_checks_target_ref="${TARGET_CONTEXT_REF:-$TARGET_REF}"', + ); + expect(releaseWorkflow.on.workflow_dispatch.inputs.target_context_ref).toMatchObject({ + required: false, + default: "", + type: "string", + }); + expect(telegramWorkflow.on.workflow_dispatch.inputs.target_context_ref).toMatchObject({ + required: false, + default: "", + type: "string", + }); expect(dispatchStep.env.TARGET_SHA).toBe("${{ needs.resolve_target.outputs.revision }}"); - expect(dispatchStep.run).toContain('telegram_target_ref="$TARGET_REF"'); - expect(dispatchStep.run).toContain("validation/target-* | refs/heads/validation/target-*)"); - expect(dispatchStep.run).toContain('telegram_target_ref="$TARGET_SHA"'); - expect(dispatchStep.run).toContain('-f target_ref="$telegram_target_ref"'); - expect(dispatchStep.run).not.toContain("release/* | refs/heads/release/*)"); + expect(dispatchStep.env.TARGET_CONTEXT_REF).toBe("${{ inputs.target_context_ref }}"); + expect(dispatchStep.run).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"'); + expect(dispatchStep.run).toContain('-f target_ref="$TARGET_SHA"'); + expect(dispatchStep.run).not.toContain("telegram_target_ref="); + expect(identityStep.run).toContain( + "Telegram QA target context must be a canonical release branch or tag.", + ); + expect(identityStep.run).toContain( + "Telegram QA release context requires an exact-SHA target ref.", + ); + for (const provenanceStep of provenanceSteps) { + expect(provenanceStep.env.TARGET_CONTEXT_REF).toBe("${{ inputs.target_context_ref }}"); + expect(provenanceStep.run).toContain("frozen-release-branch-head"); + expect(provenanceStep.run).toContain( + 'elif [[ "$candidate_version" =~ ^${release_version_pattern}-beta\\.[0-9]+$ ]]', + ); + expect(provenanceStep.run).toContain( + 'frozen_release_branch_pattern="^release/${candidate_version_pattern}-code-frozen(-r[1-9][0-9]*)?$"', + ); + expect(provenanceStep.run).toContain('elif [[ -z "$frozen_release_branch_pattern" ]]; then'); + expect(provenanceStep.run).toContain( + "Telegram candidate version ${candidate_version} does not belong to release ${release_version}.", + ); + expect(provenanceStep.run).toContain( + "Telegram candidate version ${candidate_version} does not match context ${normalized_context_ref}.", + ); + expect(provenanceStep.run).toContain('context_release_branch="$normalized_context_ref"'); + expect(provenanceStep.run).toContain('context_release_tag="$normalized_context_ref"'); + expect(provenanceStep.run).toContain( + "Frozen release candidate ${candidate_sha} requires a valid maintainer signature.", + ); + expect(provenanceStep.run).toContain( + 'select(.state == "OPEN" and .headRepository.nameWithOwner == $repo and', + ); + } }); it("keeps maturity scorecard release docs opt-in from release checks", () => { diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 6f5665e37e0d..6e14d02fda28 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -2376,6 +2376,14 @@ docker_e2e_docker_run_cmd run demo expect(publishedRunner).toContain( 'assert-prepublish-requests "$OPENCLAW_CLAWHUB_URL" "$prepublish_package" "$candidate_version"', ); + expect(publishedRunner).toContain( + 'local tarball="$fixture_root/openclaw-brave-plugin-${candidate_version}.tgz"', + ); + expect(publishedRunner).toContain('FIXTURE_PACKAGE_VERSION="$candidate_version"'); + expect(publishedRunner).toContain("version,"); + expect(publishedRunner).toContain( + 'registry_args+=("@openclaw/brave-plugin" "$candidate_version" "$tarball")', + ); expect(publishedRunner).toContain('"$clawhub_security_mode"'); expect(publishedRunner.indexOf("phase assert-prepublish-requests node")).toBeLessThan( publishedRunner.indexOf("phase doctor run_doctor"), @@ -3328,6 +3336,7 @@ grep -Fxq preserved "$TMPDIR/caller-fd" it("wires the Codex npm plugin live assertion boundary into Docker", () => { const runner = readFileSync(CODEX_NPM_PLUGIN_LIVE_DOCKER_E2E_PATH, "utf8"); + const assertions = readFileSync("scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs", "utf8"); expectTextToIncludeAll(runner, [ "docker_e2e_print_log /tmp/openclaw-codex-plugin-pack.log", "scripts/e2e/lib/plugins/npm-registry-server.mjs", @@ -3350,6 +3359,11 @@ grep -Fxq preserved "$TMPDIR/caller-fd" expect(runner).not.toContain("trap 'openclaw_e2e_stop_process \"${registry_pid:-}\"' EXIT"); expect(runner).not.toContain("final=false"); expect(runner).not.toContain("--timeout 420"); + expectTextToIncludeAll(assertions, [ + 'Requested agent harness "codex" is not registered', + "Unknown model: codex/", + 'Agent harness runtime "codex" is not present in the prepared registry.', + ]); }); it("prints the OpenAI chat-tools gateway log when startup exits early", () => { diff --git a/test/scripts/openclaw-release-telegram-qa-workflow.test.ts b/test/scripts/openclaw-release-telegram-qa-workflow.test.ts index e729298c106c..a45cfbaf57fa 100644 --- a/test/scripts/openclaw-release-telegram-qa-workflow.test.ts +++ b/test/scripts/openclaw-release-telegram-qa-workflow.test.ts @@ -75,6 +75,7 @@ function runIdentityVerification(params: { invocation?: "dispatch" | "reusable"; oidcJobWorkflowSha?: string; oidcWorkflowSha?: string; + targetContextRef?: string; workflowSha?: string; }) { const repository = "openclaw/openclaw"; @@ -143,7 +144,8 @@ function runIdentityVerification(params: { workflow_sha: params.expectedTrustedWorkflowSha, }), PATH: `${fakeBin}:${process.env.PATH}`, - TARGET_REF: "refs/heads/release/2026.7.1", + TARGET_CONTEXT_REF: params.targetContextRef ?? "", + TARGET_REF: params.targetContextRef ? "a".repeat(40) : "refs/heads/release/2026.7.1", TARGET_SHA: "a".repeat(40), WORKFLOW_REF: workflowRef, WORKFLOW_SHA: workflowSha, @@ -210,11 +212,33 @@ function runAdvisoryStatus(overrides: Record = {}) { }; } -function runCandidateProvenance(params: { openPr?: boolean; unsignedWebFlow?: boolean } = {}) { +function runCandidateProvenance( + params: { + branchHead?: string; + candidateVersion?: string; + openPr?: boolean; + remoteSha?: string; + targetContextRef?: string; + unsignedWebFlow?: boolean; + } = {}, +) { const candidateSha = "a".repeat(40); + const targetContextRef = params.targetContextRef ?? ""; + const normalizedContextRef = targetContextRef + .replace(/^refs\/heads\//u, "") + .replace(/^refs\/tags\//u, ""); + const branchHead = params.branchHead ?? "release/2026.7.1-beta.3-code-frozen-r1"; + const remoteRef = normalizedContextRef.startsWith("v") + ? `refs/tags/${normalizedContextRef}` + : `refs/heads/${normalizedContextRef || "release/2026.7.1"}`; const workdir = tempDirs.make("openclaw-telegram-provenance-"); const fakeBin = join(workdir, "bin"); mkdirSync(fakeBin); + mkdirSync(join(workdir, ".candidate")); + writeFileSync( + join(workdir, ".candidate", "package.json"), + JSON.stringify({ version: params.candidateVersion ?? "2026.7.1-beta.3" }), + ); const metadata = { data: { repository: { @@ -256,6 +280,7 @@ function runCandidateProvenance(params: { openPr?: boolean; unsignedWebFlow?: bo `#!/usr/bin/env bash set -euo pipefail if [[ "$*" == *"api graphql"* ]]; then printf '%s\\n' "$FAKE_METADATA"; exit 0; fi +if [[ "$*" == *"/branches-where-head"* ]]; then printf '%s\\n' "$FAKE_BRANCH_HEAD"; 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 @@ -267,7 +292,11 @@ exit 64 `#!/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 +if [[ "$*" == *"ls-remote"* ]]; then + if [[ "$*" == *"refs/tags/"* && "$FAKE_REMOTE_REF" != refs/tags/* ]]; then exit 0; fi + printf '%s\\t%s\\n' "$FAKE_REMOTE_SHA" "$FAKE_REMOTE_REF" + exit 0 +fi exit 64 `, { mode: 0o755 }, @@ -280,11 +309,15 @@ exit 64 encoding: "utf8", env: { ...process.env, + FAKE_BRANCH_HEAD: branchHead, FAKE_METADATA: JSON.stringify(metadata), + FAKE_REMOTE_REF: remoteRef, + FAKE_REMOTE_SHA: params.remoteSha ?? candidateSha, 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_CONTEXT_REF: targetContextRef, + TARGET_REF: targetContextRef ? candidateSha : "refs/heads/release/2026.7.1", TARGET_SHA: candidateSha, }, }, @@ -346,6 +379,18 @@ describe("release Telegram QA workflow", () => { it("accepts only the resolved trusted workflow identity", () => { const trustedSha = "b".repeat(40); expect(runIdentityVerification({ expectedTrustedWorkflowSha: trustedSha }).status).toBe(0); + for (const targetContextRef of [ + "release/2026.7.1", + "extended-stable/2026.7.33", + "v2026.7.1", + "v2026.7.1-beta.3", + ]) { + const accepted = runIdentityVerification({ + expectedTrustedWorkflowSha: trustedSha, + targetContextRef, + }); + expect(accepted.status, `${targetContextRef}: ${accepted.stderr}`).toBe(0); + } expect( runIdentityVerification({ expectedTrustedWorkflowSha: trustedSha, @@ -373,6 +418,64 @@ describe("release Telegram QA workflow", () => { expect(openPr.stderr).toContain("open same-repository PR head"); }); + it("requires canonical signed frozen heads for beta release contexts", () => { + const matching = runCandidateProvenance({ + candidateVersion: "2026.7.1-beta.3", + targetContextRef: "release/2026.7.1", + }); + expect(matching.status, matching.stderr).toBe(0); + + const unsigned = runCandidateProvenance({ + candidateVersion: "2026.7.1-beta.3", + targetContextRef: "release/2026.7.1", + unsignedWebFlow: true, + }); + expect(unsigned.status).toBe(1); + expect(unsigned.stderr).toContain("requires a valid maintainer signature"); + + const legacyFrozen = runCandidateProvenance({ + branchHead: "release/2026.7.1-beta.3-frozen-r1", + candidateVersion: "2026.7.1-beta.3", + targetContextRef: "release/2026.7.1", + }); + expect(legacyFrozen.status).toBe(1); + + const alpha = runCandidateProvenance({ + candidateVersion: "2026.7.1-alpha.1", + targetContextRef: "release/2026.7.1", + }); + expect(alpha.status).toBe(1); + expect(alpha.stderr).toContain( + "Telegram candidate version 2026.7.1-alpha.1 does not belong to release 2026.7.1.", + ); + }); + + it("binds every release context to candidate version and SHA", () => { + for (const [targetContextRef, candidateVersion] of [ + ["release/2026.7.1", "2026.7.1"], + ["extended-stable/2026.7.33", "2026.7.33"], + ["v2026.7.1", "2026.7.1"], + ["v2026.7.1-alpha.2", "2026.7.1-alpha.2"], + ["v2026.7.1-beta.3", "2026.7.1-beta.3"], + ] as const) { + const accepted = runCandidateProvenance({ candidateVersion, targetContextRef }); + expect(accepted.status, `${targetContextRef}: ${accepted.stderr}`).toBe(0); + + const versionMismatch = runCandidateProvenance({ + candidateVersion: "2026.8.1", + targetContextRef, + }); + expect(versionMismatch.status, targetContextRef).toBe(1); + + const shaMismatch = runCandidateProvenance({ + candidateVersion, + remoteSha: "b".repeat(40), + targetContextRef, + }); + expect(shaMismatch.status, targetContextRef).toBe(1); + } + }); + it("writes terminal evidence only for complete successful producers", () => { const success = runAdvisoryStatus(); expect(success.result.status, success.result.stderr).toBe(0); diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index d7265beb6471..31de2066b8c1 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -489,11 +489,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { expect(normalCiScript).toContain('args+=(-f historical_target_tag="$TARGET_REF")'); expect(normalCiScript).toContain('args+=(-f historical_target_tag="$TARGET_CONTEXT_REF")'); expect(normalCiScript).toContain('args+=(-f release_candidate_ref="$TARGET_CONTEXT_REF")'); - expect(releaseChecksScript).toContain( - 'release_checks_target_ref="${TARGET_CONTEXT_REF:-$TARGET_REF}"', - ); expect(releaseChecksStep.env?.TARGET_CONTEXT_REF).toBe("${{ inputs.target_context_ref }}"); - expect(releaseChecksScript).toContain('-f ref="$release_checks_target_ref"'); + expect(releaseChecksScript).toContain('-f ref="$TARGET_SHA"'); + expect(releaseChecksScript).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"'); expect(releaseChecksScript).toContain("args+=(-f allow_frozen_target_scenario_omissions=true)"); expect(releaseWorkflowSource).toContain('--arg targetContextRef "$TARGET_CONTEXT_REF"'); expect(releaseWorkflowSource).toContain("targetContextRef: $targetContextRef");