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