mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(release): preserve frozen candidate identity (#121786)
* commit 'c3e3f276f2b520ffdf28e453eb464a6f4e50dfa0': fix(release): preserve frozen candidate identity
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7107,19 +7107,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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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<string, string> = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user