Files
MeshChatX/scripts/ci/wait-github-workflows-and-upload-release.sh
T

237 lines
8.1 KiB
Bash

#!/usr/bin/env bash
# Wait for successful GitHub Actions runs for TAG, download artifacts from listed workflows,
# then create or update a GitHub release and upload binaries.
#
# Required env: TAG, GH_REPOSITORY (owner/repo), GH_PAT
# Optional: WORKFLOWS (space-separated, default: build-release.yml build-linux-release.yml)
# TIMEOUT_SEC (default 14400), POLL_INTERVAL (default 60), DRAFT (default false)
# RELEASE_BODY_FILE (path to markdown; default tries ./release-body.md from cwd)
set -euo pipefail
TAG="${TAG:?set TAG (e.g. v1.2.3 or release_1.2.3)}"
GH_REPOSITORY="${GH_REPOSITORY:?set GH_REPOSITORY to owner/repo on github.com}"
GH_PAT="${GH_PAT:?set GH_PAT (fine-grained or classic PAT with contents:write, actions:read)}"
WORKFLOWS="${WORKFLOWS:-build-release.yml build-linux-release.yml}"
TIMEOUT_SEC="${TIMEOUT_SEC:-14400}"
POLL_INTERVAL="${POLL_INTERVAL:-60}"
DRAFT="${DRAFT:-false}"
GH_API="https://api.github.com/repos/${GH_REPOSITORY}"
AUTH=(-H "Authorization: Bearer ${GH_PAT}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28")
WORKDIR=$(mktemp -d)
trap 'rm -rf "${WORKDIR}"' EXIT
log() {
printf '%s\n' "$*" >&2
}
if ! command -v jq >/dev/null 2>&1; then
log "Error: jq is required."
exit 1
fi
enc_tag() {
printf '%s' "$1" | jq -sRr @uri
}
latest_success_run_id() {
local wf="$1"
local tag="$2"
local tag_enc run_id runs_json commit_sha runs_json2
tag_enc=$(enc_tag "$tag")
run_id=""
if runs_json=$(curl -sS -f "${AUTH[@]}" "${GH_API}/actions/workflows/${wf}/runs?event=push&branch=${tag_enc}&per_page=30" 2>/dev/null); then
run_id=$(printf '%s' "$runs_json" | jq -r '
[.workflow_runs[] | select(.conclusion == "success")]
| sort_by(.created_at) | reverse | .[0].id // empty
')
fi
if [ -z "$run_id" ]; then
if ! commit_sha=$(curl -sS -f "${AUTH[@]}" "${GH_API}/commits/${tag_enc}" | jq -r '.sha // empty'); then
commit_sha=""
fi
if [ -n "$commit_sha" ] && runs_json2=$(curl -sS -f "${AUTH[@]}" "${GH_API}/actions/workflows/${wf}/runs?per_page=80" 2>/dev/null); then
run_id=$(printf '%s' "$runs_json2" | jq -r --arg sha "$commit_sha" '
[.workflow_runs[] | select(.head_sha == $sha and .conclusion == "success")]
| sort_by(.created_at) | reverse | .[0].id // empty
')
fi
fi
printf '%s' "$run_id"
}
deadline=$(( $(date +%s) + TIMEOUT_SEC ))
declare -A RUN_IDS=()
log "Waiting for workflows (${WORKFLOWS}) on tag ${TAG} (timeout ${TIMEOUT_SEC}s)..."
while [ "$(date +%s)" -lt "$deadline" ]; do
all_set=1
for wf in $WORKFLOWS; do
rid=$(latest_success_run_id "$wf" "$TAG")
if [ -n "$rid" ]; then
RUN_IDS["$wf"]=$rid
else
all_set=0
fi
done
if [ "$all_set" = 1 ]; then
break
fi
log "Not all workflows succeeded yet; sleeping ${POLL_INTERVAL}s..."
sleep "$POLL_INTERVAL"
done
for wf in $WORKFLOWS; do
if [ -z "${RUN_IDS[$wf]:-}" ]; then
log "Timeout or missing successful run for ${wf} (tag=${TAG})."
exit 1
fi
log "Using ${wf} run_id=${RUN_IDS[$wf]}"
done
STAGE="${WORKDIR}/stage"
mkdir -p "$STAGE"
download_and_stage_run() {
local wf="$1"
local rid="$2"
local art_json n
art_json=$(curl -sS "${AUTH[@]}" "${GH_API}/actions/runs/${rid}/artifacts?per_page=100" || true)
n=$(printf '%s' "${art_json:-{}}" | jq -r '(.artifacts // []) | length')
if [ "${n:-0}" -eq 0 ]; then
log "No artifacts for ${wf} run ${rid}."
return
fi
printf '%s' "$art_json" | jq -r '.artifacts[] | "\(.name)|\(.archive_download_url)"' | while IFS='|' read -r art_name dl_url; do
case "$wf" in
build-release.yml)
case "$art_name" in
meshchatx-windows-*|meshchatx-macos-*) ;;
*)
log "Skipping artifact ${art_name} for ${wf}"
continue
;;
esac
;;
build-linux-release.yml)
case "$art_name" in
meshchatx-linux-release-*) ;;
*)
log "Skipping artifact ${art_name} for ${wf}"
continue
;;
esac
;;
*)
log "Unknown workflow file in download filter: ${wf}" >&2
exit 1
;;
esac
zip_path="${WORKDIR}/$(echo "$art_name" | tr '/' '_').zip"
log "Downloading ${art_name}..."
if ! curl -sS -fL "${AUTH[@]}" -o "$zip_path" "$dl_url"; then
log "Warning: download failed for ${art_name}"
continue
fi
ex="${WORKDIR}/ex-${art_name}"
mkdir -p "$ex"
if ! unzip -q -o "$zip_path" -d "$ex" 2>/dev/null; then
log "Warning: unzip failed for ${art_name}"
continue
fi
find "$ex" -type f \( \
-name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' -o -name '*.whl' -o \
-name '*.exe' -o -name '*.dmg' -o -name '*.blockmap' -o \
-name 'latest*.yml' -o -name 'latest*.yaml' -o -name '*-linux.yml' -o -name '*-linux.yaml' -o \
-name 'meshchatx-frontend.zip' -o -name 'sbom.cyclonedx.json' -o -name '*.cosign.bundle' -o -name '*.intoto.jsonl' \
\) -print0 2>/dev/null | while IFS= read -r -d '' f; do
cp -f "$f" "${STAGE}/$(basename "$f")"
done
done
}
for wf in $WORKFLOWS; do
download_and_stage_run "$wf" "${RUN_IDS[$wf]}"
done
file_count=$(find "$STAGE" -mindepth 1 -maxdepth 1 -type f 2>/dev/null | wc -l)
file_count=${file_count//[[:space:]]/}
if [ "${file_count:-0}" -eq 0 ]; then
log "No release files staged after downloads."
exit 1
fi
BODY_FILE="${RELEASE_BODY_FILE:-}"
if [ -z "$BODY_FILE" ] && [ -f "./release-body.md" ]; then
BODY_FILE="./release-body.md"
fi
BODY_JSON=$(jq -n '""')
if [ -n "$BODY_FILE" ] && [ -f "$BODY_FILE" ]; then
BODY_JSON=$(jq -Rs . < "$BODY_FILE")
fi
log "Creating or locating GitHub release for tag ${TAG}..."
REL_GET=$(curl -sS -w '%{http_code}' -o "${WORKDIR}/rel.json" "${AUTH[@]}" "${GH_API}/releases/tags/${TAG}" || true)
HTTP="${REL_GET: -3}"
REL_ID=""
if [ "$HTTP" = "200" ]; then
REL_ID=$(jq -r '.id // empty' "${WORKDIR}/rel.json")
UPLOAD_URL=$(jq -r '.upload_url // empty' "${WORKDIR}/rel.json")
log "Release exists id=${REL_ID}"
else
if [ "$DRAFT" = "true" ] || [ "$DRAFT" = "1" ]; then
draft_json="true"
else
draft_json="false"
fi
payload=$(jq -n \
--arg tag "$TAG" \
--arg name "$TAG" \
--argjson draft "$draft_json" \
--argjson body "$BODY_JSON" \
'{tag_name: $tag, name: $name, body: body, draft: draft}')
if ! curl -sS -f "${AUTH[@]}" -X POST -H "Content-Type: application/json" \
-d "$payload" "${GH_API}/releases" -o "${WORKDIR}/newrel.json"; then
log "Failed to create release for ${TAG}"
exit 1
fi
REL_ID=$(jq -r '.id // empty' "${WORKDIR}/newrel.json")
UPLOAD_URL=$(jq -r '.upload_url // empty' "${WORKDIR}/newrel.json")
log "Created release id=${REL_ID}"
fi
if [ -z "$REL_ID" ] || [ -z "$UPLOAD_URL" ]; then
log "Could not resolve release id or upload_url."
exit 1
fi
BASE_UPLOAD="${UPLOAD_URL%\{*}"
EXISTING_NAMES=$(curl -sS "${AUTH[@]}" "${GH_API}/releases/${REL_ID}/assets" | jq -c '[.[].name]' 2>/dev/null || echo '[]')
while IFS= read -r -d '' f; do
base=$(basename "$f")
if jq -n -e --argjson names "$EXISTING_NAMES" --arg n "$base" '$names | index($n) != null' >/dev/null 2>&1; then
log "Skipping existing asset: ${base}"
continue
fi
enc=$(printf '%s' "$base" | jq -sRr @uri)
log "Uploading ${base}..."
if ! curl -sS -f "${AUTH[@]}" -X POST \
-H "Content-Type: application/octet-stream" \
--data-binary @"$f" \
"${BASE_UPLOAD}?name=${enc}" >/dev/null; then
log "Upload failed for ${base}"
exit 1
fi
done < <(find "$STAGE" -maxdepth 1 -type f -print0)
log "Done publishing assets to GitHub release ${TAG}."