mesa-react / scripts /release_windows.sh
Guilherme Silberfarb Costa
Add local token-aware release helpers
9a066b9
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_TOKENS_FILE="${LOCAL_TOKENS_FILE:-$SCRIPT_DIR/local_tokens.sh}"
if [[ -f "$LOCAL_TOKENS_FILE" ]]; then
# shellcheck disable=SC1090
source "$LOCAL_TOKENS_FILE"
fi
REMOTE="${REMOTE:-github}"
BASE_BRANCH="${BASE_BRANCH:-main}"
WINDOWS_BRANCH="${WINDOWS_BRANCH:-windows}"
WORKFLOW_FILE="${WORKFLOW_FILE:-build-windows-portable.yml}"
ARTIFACT_NAME="${ARTIFACT_NAME:-MesaFrame-portable}"
WAIT_FOR_RUN=1
DOWNLOAD_ARTIFACT=1
DOWNLOAD_DIR=""
POLL_SECONDS=10
usage() {
cat <<'EOF'
Uso:
./scripts/release_windows.sh [opcoes]
O script:
1. troca para a branch windows
2. faz merge da main
3. faz push da windows para o GitHub
4. dispara o workflow build-windows-portable
5. espera a execucao terminar
6. baixa o zip final para release/windows-gh/
Opcoes:
--no-wait Dispara o workflow e encerra sem esperar
--no-download Nao baixa o artefato ao final
--download-dir DIR Diretorio onde salvar o zip final
--remote NAME Remote GitHub (padrao: github)
--base-branch NAME Branch base compartilhada (padrao: main)
--windows-branch NAME Branch de release Windows (padrao: windows)
--workflow FILE Workflow a disparar (padrao: build-windows-portable.yml)
--help Mostra esta ajuda
Autenticacao:
- Se existir scripts/local_tokens.sh, ele sera carregado automaticamente.
- Se o comando 'gh' estiver instalado e autenticado, ele sera usado.
- Caso contrario, defina GH_TOKEN ou GITHUB_TOKEN para usar a API do GitHub.
EOF
}
log() {
printf '[release-windows] %s\n' "$*"
}
fail() {
printf '[release-windows] erro: %s\n' "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "comando obrigatorio ausente: $1"
}
repo_root() {
git rev-parse --show-toplevel
}
repo_slug_from_remote() {
local remote_url
remote_url="$(git remote get-url "$REMOTE")"
case "$remote_url" in
git@github.com:*)
remote_url="${remote_url#git@github.com:}"
remote_url="${remote_url%.git}"
;;
https://github.com/*)
remote_url="${remote_url#https://github.com/}"
remote_url="${remote_url%.git}"
;;
*)
fail "nao consegui identificar owner/repo a partir do remote '$REMOTE': $remote_url"
;;
esac
printf '%s\n' "$remote_url"
}
ensure_clean_tracked_tree() {
git diff --quiet || fail "ha alteracoes rastreadas nao commitadas; limpe a arvore antes de gerar a release"
git diff --cached --quiet || fail "ha alteracoes staged; finalize o commit antes de gerar a release"
}
ensure_branch_exists() {
git show-ref --verify --quiet "refs/heads/$1" || fail "branch local inexistente: $1"
}
auth_mode() {
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
printf 'gh\n'
return 0
fi
if [[ -n "${GH_TOKEN:-}" || -n "${GITHUB_TOKEN:-}" ]]; then
printf 'token\n'
return 0
fi
fail "configure o comando 'gh' com 'gh auth login' ou exporte GH_TOKEN/GITHUB_TOKEN"
}
github_api_get() {
local path="$1"
local mode="$2"
local slug="$3"
if [[ "$mode" == "gh" ]]; then
gh api "repos/$slug/$path"
return
fi
local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
curl --fail --silent --show-error --location \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $token" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/$slug/$path"
}
github_api_post() {
local path="$1"
local mode="$2"
local slug="$3"
local body="$4"
if [[ "$mode" == "gh" ]]; then
gh api --method POST "repos/$slug/$path" --input - <<<"$body" >/dev/null
return
fi
local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
curl --fail --silent --show-error --location \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $token" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-d "$body" \
"https://api.github.com/repos/$slug/$path" >/dev/null
}
download_url() {
local url="$1"
local destination="$2"
local mode="$3"
if [[ "$mode" == "gh" ]]; then
GH_FORCE_TTY=0 gh api "$url" >"$destination"
return
fi
local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
curl --fail --silent --show-error --location \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $token" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/$url" >"$destination"
}
find_run_id() {
local slug="$1"
local mode="$2"
local head_sha="$3"
local attempts=0
local max_attempts=60
while (( attempts < max_attempts )); do
local payload
payload="$(github_api_get "actions/workflows/$WORKFLOW_FILE/runs?branch=$WINDOWS_BRANCH&event=workflow_dispatch&per_page=20" "$mode" "$slug")"
local run_id
run_id="$(
python3 -c '
import json
import sys
target_sha = sys.argv[1]
payload = json.load(sys.stdin)
for run in payload.get("workflow_runs", []):
if str(run.get("head_sha") or "").strip() == target_sha:
print(run["id"])
break
' "$head_sha" <<<"$payload"
)"
if [[ -n "$run_id" ]]; then
printf '%s\n' "$run_id"
return 0
fi
sleep "$POLL_SECONDS"
attempts=$((attempts + 1))
done
fail "nao encontrei a execucao do workflow para o commit $head_sha"
}
watch_run() {
local slug="$1"
local mode="$2"
local run_id="$3"
if [[ "$mode" == "gh" ]]; then
gh run watch "$run_id" --repo "$slug" --interval "$POLL_SECONDS"
return
fi
local last_status=""
while true; do
local payload
payload="$(github_api_get "actions/runs/$run_id" "$mode" "$slug")"
local parsed
parsed="$(
python3 -c '
import json
import sys
payload = json.load(sys.stdin)
print(payload.get("status") or "")
print(payload.get("conclusion") or "")
print(payload.get("html_url") or "")
' <<<"$payload"
)"
local status conclusion html_url
status="$(printf '%s\n' "$parsed" | sed -n '1p')"
conclusion="$(printf '%s\n' "$parsed" | sed -n '2p')"
html_url="$(printf '%s\n' "$parsed" | sed -n '3p')"
if [[ "$status" != "$last_status" ]]; then
log "workflow status: ${status:-desconhecido}"
[[ -n "$html_url" ]] && log "acompanhe em: $html_url"
last_status="$status"
fi
if [[ "$status" == "completed" ]]; then
[[ "$conclusion" == "success" ]] || fail "workflow concluiu com status '$conclusion'"
return 0
fi
sleep "$POLL_SECONDS"
done
}
download_final_zip() {
local slug="$1"
local mode="$2"
local run_id="$3"
local output_dir="$4"
mkdir -p "$output_dir"
local payload
payload="$(github_api_get "actions/runs/$run_id/artifacts?per_page=100" "$mode" "$slug")"
local download_path
download_path="$(
python3 -c '
import json
import sys
artifact_name = sys.argv[1]
payload = json.load(sys.stdin)
for artifact in payload.get("artifacts", []):
if artifact.get("name") == artifact_name:
url = artifact.get("archive_download_url") or ""
if url.startswith("https://api.github.com/"):
url = url.removeprefix("https://api.github.com/")
print(url)
break
' "$ARTIFACT_NAME" <<<"$payload"
)"
[[ -n "$download_path" ]] || fail "nao encontrei o artefato '$ARTIFACT_NAME' no run $run_id"
local temp_dir
temp_dir="$(mktemp -d)"
local outer_zip="$temp_dir/github-artifact.zip"
local extracted_dir="$temp_dir/extracted"
mkdir -p "$extracted_dir"
download_url "$download_path" "$outer_zip" "$mode"
python3 - "$outer_zip" "$extracted_dir" <<'PY'
import sys
from zipfile import ZipFile
with ZipFile(sys.argv[1]) as archive:
archive.extractall(sys.argv[2])
PY
local inner_zip
inner_zip="$(find "$extracted_dir" -type f -name 'MesaFrame-portable.zip' | head -n 1)"
[[ -n "$inner_zip" ]] || fail "nao encontrei o zip final dentro do artefato baixado"
local final_zip="$output_dir/MesaFrame-portable-run-${run_id}.zip"
cp "$inner_zip" "$final_zip"
log "zip final salvo em: $final_zip"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--no-wait)
WAIT_FOR_RUN=0
shift
;;
--no-download)
DOWNLOAD_ARTIFACT=0
shift
;;
--download-dir)
[[ $# -ge 2 ]] || fail "faltou valor para --download-dir"
DOWNLOAD_DIR="$2"
shift 2
;;
--remote)
[[ $# -ge 2 ]] || fail "faltou valor para --remote"
REMOTE="$2"
shift 2
;;
--base-branch)
[[ $# -ge 2 ]] || fail "faltou valor para --base-branch"
BASE_BRANCH="$2"
shift 2
;;
--windows-branch)
[[ $# -ge 2 ]] || fail "faltou valor para --windows-branch"
WINDOWS_BRANCH="$2"
shift 2
;;
--workflow)
[[ $# -ge 2 ]] || fail "faltou valor para --workflow"
WORKFLOW_FILE="$2"
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
fail "opcao desconhecida: $1"
;;
esac
done
require_cmd git
require_cmd curl
require_cmd python3
ROOT_DIR="$(repo_root)"
cd "$ROOT_DIR"
MODE="$(auth_mode)"
SLUG="$(repo_slug_from_remote)"
ORIGINAL_BRANCH="$(git branch --show-current)"
[[ -n "$DOWNLOAD_DIR" ]] || DOWNLOAD_DIR="$ROOT_DIR/release/windows-gh"
ensure_branch_exists "$BASE_BRANCH"
ensure_branch_exists "$WINDOWS_BRANCH"
ensure_clean_tracked_tree
git cat-file -e "${WINDOWS_BRANCH}:.github/workflows/${WORKFLOW_FILE}" 2>/dev/null || fail "workflow nao encontrado na branch '$WINDOWS_BRANCH': .github/workflows/$WORKFLOW_FILE"
log "repositorio: $SLUG"
log "branch base: $BASE_BRANCH"
log "branch windows: $WINDOWS_BRANCH"
git checkout "$WINDOWS_BRANCH" >/dev/null
git merge --no-edit "$BASE_BRANCH"
HEAD_SHA="$(git rev-parse HEAD)"
log "push da branch $WINDOWS_BRANCH"
git push "$REMOTE" "$WINDOWS_BRANCH:$WINDOWS_BRANCH"
log "disparando workflow $WORKFLOW_FILE"
github_api_post "actions/workflows/$WORKFLOW_FILE/dispatches" "$MODE" "$SLUG" "{\"ref\":\"$WINDOWS_BRANCH\"}"
RUN_ID="$(find_run_id "$SLUG" "$MODE" "$HEAD_SHA")"
log "run id: $RUN_ID"
if (( WAIT_FOR_RUN == 1 )); then
watch_run "$SLUG" "$MODE" "$RUN_ID"
if (( DOWNLOAD_ARTIFACT == 1 )); then
download_final_zip "$SLUG" "$MODE" "$RUN_ID" "$DOWNLOAD_DIR"
fi
else
log "workflow disparado; como --no-wait foi usado, encerrando sem acompanhar"
fi
if [[ -n "$ORIGINAL_BRANCH" && "$ORIGINAL_BRANCH" != "$WINDOWS_BRANCH" ]]; then
git checkout "$ORIGINAL_BRANCH" >/dev/null
fi
log "concluido"