Guilherme Silberfarb Costa commited on
Commit
613a5ea
·
1 Parent(s): 3c84c35

Add helper script for Windows release workflow

Browse files
Files changed (1) hide show
  1. scripts/release_windows.sh +414 -0
scripts/release_windows.sh ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ REMOTE="${REMOTE:-github}"
6
+ BASE_BRANCH="${BASE_BRANCH:-main}"
7
+ WINDOWS_BRANCH="${WINDOWS_BRANCH:-windows}"
8
+ WORKFLOW_FILE="${WORKFLOW_FILE:-build-windows-portable.yml}"
9
+ ARTIFACT_NAME="${ARTIFACT_NAME:-MesaFrame-portable}"
10
+ WAIT_FOR_RUN=1
11
+ DOWNLOAD_ARTIFACT=1
12
+ DOWNLOAD_DIR=""
13
+ POLL_SECONDS=10
14
+
15
+
16
+ usage() {
17
+ cat <<'EOF'
18
+ Uso:
19
+ ./scripts/release_windows.sh [opcoes]
20
+
21
+ O script:
22
+ 1. troca para a branch windows
23
+ 2. faz merge da main
24
+ 3. faz push da windows para o GitHub
25
+ 4. dispara o workflow build-windows-portable
26
+ 5. espera a execucao terminar
27
+ 6. baixa o zip final para release/windows-gh/
28
+
29
+ Opcoes:
30
+ --no-wait Dispara o workflow e encerra sem esperar
31
+ --no-download Nao baixa o artefato ao final
32
+ --download-dir DIR Diretorio onde salvar o zip final
33
+ --remote NAME Remote GitHub (padrao: github)
34
+ --base-branch NAME Branch base compartilhada (padrao: main)
35
+ --windows-branch NAME Branch de release Windows (padrao: windows)
36
+ --workflow FILE Workflow a disparar (padrao: build-windows-portable.yml)
37
+ --help Mostra esta ajuda
38
+
39
+ Autenticacao:
40
+ - Se o comando 'gh' estiver instalado e autenticado, ele sera usado.
41
+ - Caso contrario, defina GH_TOKEN ou GITHUB_TOKEN para usar a API do GitHub.
42
+ EOF
43
+ }
44
+
45
+
46
+ log() {
47
+ printf '[release-windows] %s\n' "$*"
48
+ }
49
+
50
+
51
+ fail() {
52
+ printf '[release-windows] erro: %s\n' "$*" >&2
53
+ exit 1
54
+ }
55
+
56
+
57
+ require_cmd() {
58
+ command -v "$1" >/dev/null 2>&1 || fail "comando obrigatorio ausente: $1"
59
+ }
60
+
61
+
62
+ repo_root() {
63
+ git rev-parse --show-toplevel
64
+ }
65
+
66
+
67
+ repo_slug_from_remote() {
68
+ local remote_url
69
+ remote_url="$(git remote get-url "$REMOTE")"
70
+
71
+ case "$remote_url" in
72
+ git@github.com:*)
73
+ remote_url="${remote_url#git@github.com:}"
74
+ remote_url="${remote_url%.git}"
75
+ ;;
76
+ https://github.com/*)
77
+ remote_url="${remote_url#https://github.com/}"
78
+ remote_url="${remote_url%.git}"
79
+ ;;
80
+ *)
81
+ fail "nao consegui identificar owner/repo a partir do remote '$REMOTE': $remote_url"
82
+ ;;
83
+ esac
84
+
85
+ printf '%s\n' "$remote_url"
86
+ }
87
+
88
+
89
+ ensure_clean_tracked_tree() {
90
+ git diff --quiet || fail "ha alteracoes rastreadas nao commitadas; limpe a arvore antes de gerar a release"
91
+ git diff --cached --quiet || fail "ha alteracoes staged; finalize o commit antes de gerar a release"
92
+ }
93
+
94
+
95
+ ensure_branch_exists() {
96
+ git show-ref --verify --quiet "refs/heads/$1" || fail "branch local inexistente: $1"
97
+ }
98
+
99
+
100
+ auth_mode() {
101
+ if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
102
+ printf 'gh\n'
103
+ return 0
104
+ fi
105
+
106
+ if [[ -n "${GH_TOKEN:-}" || -n "${GITHUB_TOKEN:-}" ]]; then
107
+ printf 'token\n'
108
+ return 0
109
+ fi
110
+
111
+ fail "configure o comando 'gh' com 'gh auth login' ou exporte GH_TOKEN/GITHUB_TOKEN"
112
+ }
113
+
114
+
115
+ github_api_get() {
116
+ local path="$1"
117
+ local mode="$2"
118
+ local slug="$3"
119
+
120
+ if [[ "$mode" == "gh" ]]; then
121
+ gh api "repos/$slug/$path"
122
+ return
123
+ fi
124
+
125
+ local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
126
+ curl --fail --silent --show-error --location \
127
+ -H "Accept: application/vnd.github+json" \
128
+ -H "Authorization: Bearer $token" \
129
+ -H "X-GitHub-Api-Version: 2022-11-28" \
130
+ "https://api.github.com/repos/$slug/$path"
131
+ }
132
+
133
+
134
+ github_api_post() {
135
+ local path="$1"
136
+ local mode="$2"
137
+ local slug="$3"
138
+ local body="$4"
139
+
140
+ if [[ "$mode" == "gh" ]]; then
141
+ gh api --method POST "repos/$slug/$path" --input - <<<"$body" >/dev/null
142
+ return
143
+ fi
144
+
145
+ local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
146
+ curl --fail --silent --show-error --location \
147
+ -X POST \
148
+ -H "Accept: application/vnd.github+json" \
149
+ -H "Authorization: Bearer $token" \
150
+ -H "X-GitHub-Api-Version: 2022-11-28" \
151
+ -d "$body" \
152
+ "https://api.github.com/repos/$slug/$path" >/dev/null
153
+ }
154
+
155
+
156
+ download_url() {
157
+ local url="$1"
158
+ local destination="$2"
159
+ local mode="$3"
160
+
161
+ if [[ "$mode" == "gh" ]]; then
162
+ GH_FORCE_TTY=0 gh api "$url" >"$destination"
163
+ return
164
+ fi
165
+
166
+ local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
167
+ curl --fail --silent --show-error --location \
168
+ -H "Accept: application/vnd.github+json" \
169
+ -H "Authorization: Bearer $token" \
170
+ -H "X-GitHub-Api-Version: 2022-11-28" \
171
+ "https://api.github.com/$url" >"$destination"
172
+ }
173
+
174
+
175
+ find_run_id() {
176
+ local slug="$1"
177
+ local mode="$2"
178
+ local head_sha="$3"
179
+ local attempts=0
180
+ local max_attempts=60
181
+
182
+ while (( attempts < max_attempts )); do
183
+ local payload
184
+ payload="$(github_api_get "actions/workflows/$WORKFLOW_FILE/runs?branch=$WINDOWS_BRANCH&event=workflow_dispatch&per_page=20" "$mode" "$slug")"
185
+
186
+ local run_id
187
+ run_id="$(
188
+ python3 -c '
189
+ import json
190
+ import sys
191
+
192
+ target_sha = sys.argv[1]
193
+ payload = json.load(sys.stdin)
194
+ for run in payload.get("workflow_runs", []):
195
+ if str(run.get("head_sha") or "").strip() == target_sha:
196
+ print(run["id"])
197
+ break
198
+ ' "$head_sha" <<<"$payload"
199
+ )"
200
+
201
+ if [[ -n "$run_id" ]]; then
202
+ printf '%s\n' "$run_id"
203
+ return 0
204
+ fi
205
+
206
+ sleep "$POLL_SECONDS"
207
+ attempts=$((attempts + 1))
208
+ done
209
+
210
+ fail "nao encontrei a execucao do workflow para o commit $head_sha"
211
+ }
212
+
213
+
214
+ watch_run() {
215
+ local slug="$1"
216
+ local mode="$2"
217
+ local run_id="$3"
218
+
219
+ if [[ "$mode" == "gh" ]]; then
220
+ gh run watch "$run_id" --repo "$slug" --interval "$POLL_SECONDS"
221
+ return
222
+ fi
223
+
224
+ local last_status=""
225
+ while true; do
226
+ local payload
227
+ payload="$(github_api_get "actions/runs/$run_id" "$mode" "$slug")"
228
+
229
+ local parsed
230
+ parsed="$(
231
+ python3 -c '
232
+ import json
233
+ import sys
234
+
235
+ payload = json.load(sys.stdin)
236
+ print(payload.get("status") or "")
237
+ print(payload.get("conclusion") or "")
238
+ print(payload.get("html_url") or "")
239
+ ' <<<"$payload"
240
+ )"
241
+
242
+ local status conclusion html_url
243
+ status="$(printf '%s\n' "$parsed" | sed -n '1p')"
244
+ conclusion="$(printf '%s\n' "$parsed" | sed -n '2p')"
245
+ html_url="$(printf '%s\n' "$parsed" | sed -n '3p')"
246
+
247
+ if [[ "$status" != "$last_status" ]]; then
248
+ log "workflow status: ${status:-desconhecido}"
249
+ [[ -n "$html_url" ]] && log "acompanhe em: $html_url"
250
+ last_status="$status"
251
+ fi
252
+
253
+ if [[ "$status" == "completed" ]]; then
254
+ [[ "$conclusion" == "success" ]] || fail "workflow concluiu com status '$conclusion'"
255
+ return 0
256
+ fi
257
+
258
+ sleep "$POLL_SECONDS"
259
+ done
260
+ }
261
+
262
+
263
+ download_final_zip() {
264
+ local slug="$1"
265
+ local mode="$2"
266
+ local run_id="$3"
267
+ local output_dir="$4"
268
+
269
+ mkdir -p "$output_dir"
270
+
271
+ local payload
272
+ payload="$(github_api_get "actions/runs/$run_id/artifacts?per_page=100" "$mode" "$slug")"
273
+
274
+ local download_path
275
+ download_path="$(
276
+ python3 -c '
277
+ import json
278
+ import sys
279
+
280
+ artifact_name = sys.argv[1]
281
+ payload = json.load(sys.stdin)
282
+ for artifact in payload.get("artifacts", []):
283
+ if artifact.get("name") == artifact_name:
284
+ url = artifact.get("archive_download_url") or ""
285
+ if url.startswith("https://api.github.com/"):
286
+ url = url.removeprefix("https://api.github.com/")
287
+ print(url)
288
+ break
289
+ ' "$ARTIFACT_NAME" <<<"$payload"
290
+ )"
291
+
292
+ [[ -n "$download_path" ]] || fail "nao encontrei o artefato '$ARTIFACT_NAME' no run $run_id"
293
+
294
+ local temp_dir
295
+ temp_dir="$(mktemp -d)"
296
+ local outer_zip="$temp_dir/github-artifact.zip"
297
+ local extracted_dir="$temp_dir/extracted"
298
+ mkdir -p "$extracted_dir"
299
+
300
+ download_url "$download_path" "$outer_zip" "$mode"
301
+
302
+ python3 - "$outer_zip" "$extracted_dir" <<'PY'
303
+ import sys
304
+ from zipfile import ZipFile
305
+
306
+ with ZipFile(sys.argv[1]) as archive:
307
+ archive.extractall(sys.argv[2])
308
+ PY
309
+
310
+ local inner_zip
311
+ inner_zip="$(find "$extracted_dir" -type f -name 'MesaFrame-portable.zip' | head -n 1)"
312
+ [[ -n "$inner_zip" ]] || fail "nao encontrei o zip final dentro do artefato baixado"
313
+
314
+ local final_zip="$output_dir/MesaFrame-portable-run-${run_id}.zip"
315
+ cp "$inner_zip" "$final_zip"
316
+ log "zip final salvo em: $final_zip"
317
+ }
318
+
319
+
320
+ while [[ $# -gt 0 ]]; do
321
+ case "$1" in
322
+ --no-wait)
323
+ WAIT_FOR_RUN=0
324
+ shift
325
+ ;;
326
+ --no-download)
327
+ DOWNLOAD_ARTIFACT=0
328
+ shift
329
+ ;;
330
+ --download-dir)
331
+ [[ $# -ge 2 ]] || fail "faltou valor para --download-dir"
332
+ DOWNLOAD_DIR="$2"
333
+ shift 2
334
+ ;;
335
+ --remote)
336
+ [[ $# -ge 2 ]] || fail "faltou valor para --remote"
337
+ REMOTE="$2"
338
+ shift 2
339
+ ;;
340
+ --base-branch)
341
+ [[ $# -ge 2 ]] || fail "faltou valor para --base-branch"
342
+ BASE_BRANCH="$2"
343
+ shift 2
344
+ ;;
345
+ --windows-branch)
346
+ [[ $# -ge 2 ]] || fail "faltou valor para --windows-branch"
347
+ WINDOWS_BRANCH="$2"
348
+ shift 2
349
+ ;;
350
+ --workflow)
351
+ [[ $# -ge 2 ]] || fail "faltou valor para --workflow"
352
+ WORKFLOW_FILE="$2"
353
+ shift 2
354
+ ;;
355
+ --help|-h)
356
+ usage
357
+ exit 0
358
+ ;;
359
+ *)
360
+ fail "opcao desconhecida: $1"
361
+ ;;
362
+ esac
363
+ done
364
+
365
+
366
+ require_cmd git
367
+ require_cmd curl
368
+ require_cmd python3
369
+
370
+ ROOT_DIR="$(repo_root)"
371
+ cd "$ROOT_DIR"
372
+
373
+ MODE="$(auth_mode)"
374
+ SLUG="$(repo_slug_from_remote)"
375
+ ORIGINAL_BRANCH="$(git branch --show-current)"
376
+
377
+ [[ -n "$DOWNLOAD_DIR" ]] || DOWNLOAD_DIR="$ROOT_DIR/release/windows-gh"
378
+
379
+ ensure_branch_exists "$BASE_BRANCH"
380
+ ensure_branch_exists "$WINDOWS_BRANCH"
381
+ ensure_clean_tracked_tree
382
+ 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"
383
+
384
+ log "repositorio: $SLUG"
385
+ log "branch base: $BASE_BRANCH"
386
+ log "branch windows: $WINDOWS_BRANCH"
387
+
388
+ git checkout "$WINDOWS_BRANCH" >/dev/null
389
+ git merge --no-edit "$BASE_BRANCH"
390
+
391
+ HEAD_SHA="$(git rev-parse HEAD)"
392
+ log "push da branch $WINDOWS_BRANCH"
393
+ git push "$REMOTE" "$WINDOWS_BRANCH:$WINDOWS_BRANCH"
394
+
395
+ log "disparando workflow $WORKFLOW_FILE"
396
+ github_api_post "actions/workflows/$WORKFLOW_FILE/dispatches" "$MODE" "$SLUG" "{\"ref\":\"$WINDOWS_BRANCH\"}"
397
+
398
+ RUN_ID="$(find_run_id "$SLUG" "$MODE" "$HEAD_SHA")"
399
+ log "run id: $RUN_ID"
400
+
401
+ if (( WAIT_FOR_RUN == 1 )); then
402
+ watch_run "$SLUG" "$MODE" "$RUN_ID"
403
+ if (( DOWNLOAD_ARTIFACT == 1 )); then
404
+ download_final_zip "$SLUG" "$MODE" "$RUN_ID" "$DOWNLOAD_DIR"
405
+ fi
406
+ else
407
+ log "workflow disparado; como --no-wait foi usado, encerrando sem acompanhar"
408
+ fi
409
+
410
+ if [[ -n "$ORIGINAL_BRANCH" && "$ORIGINAL_BRANCH" != "$WINDOWS_BRANCH" ]]; then
411
+ git checkout "$ORIGINAL_BRANCH" >/dev/null
412
+ fi
413
+
414
+ log "concluido"