Files
openclaw/scripts/github/resolve-release-check-artifacts.sh
Vincent Koc 9518564e30 fix(ci): preserve release evidence across reruns (#119380)
Punchcard-Session: silver-valley-valley-dt
2026-08-05 08:43:25 +08:00

233 lines
9.1 KiB
Bash
Executable File

#!/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