mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
fix(ci): preserve release evidence across reruns (#119380)
Punchcard-Session: silver-valley-valley-dt
This commit is contained in:
+232
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
tool_name="resolve-release-check-artifacts"
|
||||
tmp_file=""
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
[[ -z "$tmp_file" ]] || rm -f "$tmp_file"
|
||||
if [[ "$exit_code" -ne 0 ]]; then
|
||||
echo "[${tool_name}] FAILED (exit ${exit_code})" >&2
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
echo "${tool_name}: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command_name="${1:-}"
|
||||
[[ -n "$command_name" ]] || fail "usage: ${tool_name} <resolve|validate> [options]"
|
||||
shift
|
||||
|
||||
repository=""
|
||||
run_id=""
|
||||
consumer_attempt=""
|
||||
target_sha=""
|
||||
selection_file=""
|
||||
status_dir=""
|
||||
validated_file=""
|
||||
github_output=""
|
||||
pairs=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
option="$1"
|
||||
[[ "$option" == --* && -n "${2:-}" ]] || fail "${option} requires a value"
|
||||
value="$2"
|
||||
shift 2
|
||||
case "$option" in
|
||||
--repository) repository="$value" ;;
|
||||
--run-id) run_id="$value" ;;
|
||||
--consumer-attempt) consumer_attempt="$value" ;;
|
||||
--target-sha) target_sha="$value" ;;
|
||||
--selection-file) selection_file="$value" ;;
|
||||
--status-dir) status_dir="$value" ;;
|
||||
--validated-file) validated_file="$value" ;;
|
||||
--github-output) github_output="$value" ;;
|
||||
--pair) pairs+=("$value") ;;
|
||||
*) fail "unknown ${command_name} argument: ${option}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$command_name" in
|
||||
resolve)
|
||||
[[ "$repository" =~ ^[^/]+/[^/]+$ ]] ||
|
||||
fail "--repository must use owner/repository form"
|
||||
[[ "$run_id" =~ ^[1-9][0-9]*$ ]] || fail "--run-id must be a positive integer"
|
||||
[[ "$consumer_attempt" =~ ^[1-9][0-9]*$ ]] ||
|
||||
fail "--consumer-attempt must be a positive integer"
|
||||
[[ "$target_sha" =~ ^[0-9a-f]{40}$ ]] ||
|
||||
fail "--target-sha must be a lowercase full commit SHA"
|
||||
[[ -n "$selection_file" ]] || fail "--selection-file is required"
|
||||
mkdir -p "$(dirname "$selection_file")"
|
||||
|
||||
pairs_json="[]"
|
||||
if [[ "${#pairs[@]}" -gt 0 ]]; then
|
||||
if ! pairs_json="$(
|
||||
printf '%s\n' "${pairs[@]}" | jq -Rn '
|
||||
[inputs | split("|")] |
|
||||
if all(.[];
|
||||
length == 4 and .[0] != "" and .[2] != "" and .[3] != ""
|
||||
) then
|
||||
map({job: .[0], variant: .[1], status_base: .[2], payload_base: .[3]})
|
||||
else
|
||||
error("invalid --pair value")
|
||||
end
|
||||
'
|
||||
)"; then
|
||||
fail "invalid artifact pair specification"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$pairs_json" == "[]" ]]; then
|
||||
printf '[]\n' > "$selection_file"
|
||||
elif ! gh api --paginate \
|
||||
"repos/${repository}/actions/runs/${run_id}/artifacts?per_page=100" |
|
||||
jq -e -s \
|
||||
--argjson pairs "$pairs_json" \
|
||||
--arg run_id "$run_id" \
|
||||
--arg target_sha "$target_sha" \
|
||||
--argjson consumer_attempt "$consumer_attempt" '
|
||||
def selected_artifact($matches; $name; $label):
|
||||
if ($matches | length) != 1 then
|
||||
error("\($label) requires exactly one \($name) artifact; found \($matches | length)")
|
||||
else
|
||||
$matches[0]
|
||||
| if ((.id | type) == "number" and .id > 0 and (.id | floor) == .id) then .
|
||||
else error("\($name) has a missing or invalid artifact id") end
|
||||
| if ((.workflow_run.id | tostring) == $run_id) then .
|
||||
else error("\($name) does not belong to workflow run \($run_id)") end
|
||||
| if .expired == false then .
|
||||
else error("\($name) is expired or has invalid expiry metadata") end
|
||||
end;
|
||||
def select_pair($artifacts; $pair):
|
||||
($pair.job + (if $pair.variant == "" then "" else "/" + $pair.variant end)) as $label
|
||||
| ($pair.status_base + "-") as $status_prefix
|
||||
| ($pair.payload_base + "-") as $payload_prefix
|
||||
| [
|
||||
$artifacts[]
|
||||
| select((.name | type) == "string")
|
||||
| select((.name | startswith($status_prefix)) or
|
||||
(.name | startswith($payload_prefix)))
|
||||
| (
|
||||
if (.name | startswith($status_prefix))
|
||||
then .name | ltrimstr($status_prefix)
|
||||
else .name | ltrimstr($payload_prefix)
|
||||
end) as $attempt
|
||||
| if ($attempt | test("^[1-9][0-9]*$")) then ($attempt | tonumber)
|
||||
else error("\($label) has malformed producer attempt in artifact \(.name)") end
|
||||
] as $attempts
|
||||
| [$attempts[] | select(. <= $consumer_attempt)] as $eligible
|
||||
| if ($eligible | length) == 0 then
|
||||
error("no \($label) evidence exists through consumer attempt \($consumer_attempt)")
|
||||
else
|
||||
($eligible | max) as $attempt
|
||||
| ($pair.status_base + "-" + ($attempt | tostring)) as $status_name
|
||||
| ($pair.payload_base + "-" + ($attempt | tostring)) as $payload_name
|
||||
| selected_artifact(
|
||||
[$artifacts[] | select(.name == $status_name)];
|
||||
$status_name; $label) as $status
|
||||
| selected_artifact(
|
||||
[$artifacts[] | select(.name == $payload_name)];
|
||||
$payload_name; $label) as $payload
|
||||
| {run_id: $run_id, target_sha: $target_sha, job: $pair.job,
|
||||
variant: $pair.variant, producer_attempt: $attempt,
|
||||
status_id: $status.id, status_name: $status_name,
|
||||
payload_id: $payload.id, payload_name: $payload_name}
|
||||
end;
|
||||
if all(.[]; (.artifacts | type) == "array") then
|
||||
[.[].artifacts[]] as $artifacts
|
||||
| [$pairs[] | select_pair($artifacts; .)] as $selected
|
||||
| [$selected[] | .status_id, .payload_id] as $ids
|
||||
| if ($ids | length) == ($ids | unique | length) then $selected
|
||||
else error("logical producers reuse a selected artifact id") end
|
||||
else
|
||||
error("invalid workflow artifact response")
|
||||
end
|
||||
' > "$selection_file"; then
|
||||
fail "artifact resolution failed"
|
||||
fi
|
||||
|
||||
if [[ -n "$github_output" ]]; then
|
||||
{
|
||||
printf 'selection_file=%s\n' "$selection_file"
|
||||
printf 'status_ids=%s\n' \
|
||||
"$(jq -r 'map(.status_id | tostring) | join(",")' "$selection_file")"
|
||||
printf 'payload_ids=%s\n' \
|
||||
"$(jq -r 'map(.payload_id | tostring) | join(",")' "$selection_file")"
|
||||
} >> "$github_output"
|
||||
fi
|
||||
;;
|
||||
|
||||
validate)
|
||||
[[ -f "$selection_file" ]] || fail "selection file is missing: ${selection_file}"
|
||||
[[ -d "$status_dir" ]] || fail "status directory is missing: ${status_dir}"
|
||||
[[ -n "$validated_file" ]] || fail "--validated-file is required"
|
||||
if ! jq -e '
|
||||
type == "array" and
|
||||
all(.[];
|
||||
(.run_id | type) == "string" and (.target_sha | test("^[0-9a-f]{40}$")) and
|
||||
(.job | type) == "string" and (.job | length) > 0 and (.variant | type) == "string" and
|
||||
(.producer_attempt | type) == "number" and .producer_attempt > 0 and
|
||||
(.producer_attempt | floor) == .producer_attempt and
|
||||
(.status_id | type) == "number" and .status_id > 0 and
|
||||
(.payload_id | type) == "number" and .payload_id > 0
|
||||
) and
|
||||
([.[] | [.job, .variant] | @tsv] | length) ==
|
||||
([.[] | [.job, .variant] | @tsv] | unique | length)
|
||||
' "$selection_file" >/dev/null; then
|
||||
fail "selection file has invalid or duplicate entries"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$validated_file")"
|
||||
tmp_file="$(mktemp)"
|
||||
while IFS= read -r selection; do
|
||||
expected_name="$(
|
||||
jq -r '
|
||||
.job +
|
||||
(if .variant == "" then "" else "-" + .variant end) +
|
||||
"-" + .run_id + "-" + (.producer_attempt | tostring) + ".env"
|
||||
' <<< "$selection"
|
||||
)"
|
||||
status_file="${status_dir}/${expected_name}"
|
||||
[[ -f "$status_file" ]] ||
|
||||
fail "${expected_name} requires exactly one downloaded status file; found 0"
|
||||
if ! jq -Rsc --argjson selection "$selection" '
|
||||
def field($key):
|
||||
[split("\n")[] | select(startswith($key + "=")) | .[($key | length) + 1:]]
|
||||
| if length == 1 then .[0]
|
||||
else error("\($key) is duplicate or missing") end;
|
||||
{
|
||||
run_id: field("run_id"),
|
||||
run_attempt: field("run_attempt"),
|
||||
target_sha: field("target_sha"),
|
||||
job: field("job"),
|
||||
variant: field("variant"),
|
||||
status: field("status")
|
||||
} as $actual
|
||||
| if (
|
||||
$actual.run_id == $selection.run_id and
|
||||
$actual.run_attempt == ($selection.producer_attempt | tostring) and
|
||||
$actual.target_sha == $selection.target_sha and
|
||||
$actual.job == $selection.job and
|
||||
$actual.variant == $selection.variant and
|
||||
($actual.status | IN("success", "failure", "cancelled", "skipped"))
|
||||
)
|
||||
then $selection + {status: $actual.status}
|
||||
else error("status metadata does not match selected evidence")
|
||||
end
|
||||
' "$status_file" >> "$tmp_file"; then
|
||||
fail "${expected_name} has invalid status metadata"
|
||||
fi
|
||||
done < <(jq -c '.[]' "$selection_file")
|
||||
jq -s '.' "$tmp_file" > "$validated_file"
|
||||
if [[ -n "$github_output" ]]; then
|
||||
printf 'validated_file=%s\n' "$validated_file" >> "$github_output"
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
fail "usage: ${tool_name} <resolve|validate> [options]"
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user