#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' Usage: ./scripts/release/release-publish-github.sh --version 2026.04.16-alpha.4 [options] Options: --remote Git remote name. Default: github-public --repo GitHub repository in owner/name form --token GitHub token --target-commitish Branch or commit to associate when creating the tag --notes Release notes markdown file --dry-run Print planned uploads without calling the API EOF } version="" remote_name="github-public" repo_full_name="" token="${GITHUB_RELEASE_TOKEN:-${GITHUB_TOKEN:-}}" target_commitish="" notes_path="" dry_run="false" while [[ $# -gt 0 ]]; do case "$1" in --version) version="${2:-}" shift 2 ;; --remote) remote_name="${2:-}" shift 2 ;; --repo) repo_full_name="${2:-}" shift 2 ;; --token) token="${2:-}" shift 2 ;; --target-commitish) target_commitish="${2:-}" shift 2 ;; --notes) notes_path="${2:-}" shift 2 ;; --dry-run) dry_run="true" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown argument: $1" >&2 usage >&2 exit 1 ;; esac done if [[ -z "$version" ]]; then usage >&2 exit 1 fi repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" release_root="$repo_root/release-assets/releases/$version" if [[ ! -d "$release_root" ]]; then echo "Release bundle not found: $release_root" >&2 exit 1 fi remote_url="$(git -C "$repo_root" remote get-url "$remote_name" 2>/dev/null || true)" if [[ -z "$repo_full_name" ]]; then if [[ "$remote_url" =~ ^https?://github\.com/(.+?)(\.git)?$ ]]; then repo_full_name="${BASH_REMATCH[1]}" elif [[ "$remote_url" =~ ^git@github\.com:(.+)\.git$ ]]; then repo_full_name="${BASH_REMATCH[1]}" fi fi if [[ -z "$repo_full_name" ]]; then echo "Unable to infer GitHub repository name from remote: $remote_name" >&2 exit 1 fi if [[ -z "$token" && -f "$repo_root/.workspace-secrets/github-public.pat" ]]; then token="$(tr -d '\r\n' < "$repo_root/.workspace-secrets/github-public.pat")" fi if [[ "$dry_run" != "true" && -z "$token" ]]; then echo "A GitHub token is required. Use --token, GITHUB_RELEASE_TOKEN, or GITHUB_TOKEN." >&2 exit 1 fi if [[ -z "$notes_path" ]]; then notes_path="$release_root/RELEASE_NOTES.ko.md" fi if [[ -n "$notes_path" && ! -f "$notes_path" ]]; then echo "Release notes file not found: $notes_path" >&2 exit 1 fi if [[ -z "$target_commitish" ]]; then target_commitish="main" fi pre_release="false" case "$version" in *alpha*|*beta*|*rc*) pre_release="true" ;; esac mapfile -t asset_files < <( find "$release_root" -type f \ \( -name '*.zip' -o -name '*.apk' -o -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' -o -name 'RELEASE_NOTES.ko.md' -o -name 'SHA256SUMS.txt' -o -name 'version.json' \) \ | sort ) if [[ ${#asset_files[@]} -eq 0 ]]; then echo "No release assets found in $release_root" >&2 exit 1 fi echo "GitHub release target: https://github.com/$repo_full_name" echo "Remote: $remote_name" echo "Version: $version" echo "Target commitish: $target_commitish" printf 'Assets:\n' printf ' - %s\n' "${asset_files[@]#$release_root/}" if [[ "$dry_run" == "true" ]]; then exit 0 fi api_root="https://api.github.com/repos/${repo_full_name}" release_api="${api_root}/releases" tag_api="${release_api}/tags/${version}" auth_header="Authorization: Bearer $token" accept_header="Accept: application/vnd.github+json" api_version_header="X-GitHub-Api-Version: 2022-11-28" tmp_response="$(mktemp)" trap 'rm -f "$tmp_response"' EXIT existing_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' \ -H "$auth_header" \ -H "$accept_header" \ -H "$api_version_header" \ "$tag_api")" if [[ "$existing_status" == "200" ]]; then existing_release_id="$(python3 - <<'PY' "$tmp_response" import json, sys with open(sys.argv[1], "r", encoding="utf-8") as fh: print(json.load(fh)["id"]) PY )" curl -sS -X DELETE \ -H "$auth_header" \ -H "$accept_header" \ -H "$api_version_header" \ "${release_api}/${existing_release_id}" >/dev/null fi release_body="$(cat "$notes_path")" create_payload="$(python3 - <<'PY' "$version" "$release_body" "$pre_release" "$target_commitish" import json, sys version, body, prerelease, target_commitish = sys.argv[1], sys.argv[2], sys.argv[3] == "true", sys.argv[4] print(json.dumps({ "tag_name": version, "target_commitish": target_commitish, "name": version, "body": body, "draft": False, "prerelease": prerelease, "make_latest": "false" if prerelease else "true", }, ensure_ascii=False)) PY )" create_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' \ -X POST \ -H "$auth_header" \ -H "$accept_header" \ -H "$api_version_header" \ -H 'Content-Type: application/json' \ -d "$create_payload" \ "$release_api")" if [[ "$create_status" != "201" ]]; then echo "Failed to create GitHub release. HTTP $create_status" >&2 cat "$tmp_response" >&2 exit 1 fi upload_url="$(python3 - <<'PY' "$tmp_response" import json, sys with open(sys.argv[1], "r", encoding="utf-8") as fh: print(json.load(fh)["upload_url"].split("{", 1)[0]) PY )" for asset in "${asset_files[@]}"; do name="$(basename "$asset")" curl -sS \ -X POST \ -H "$auth_header" \ -H "$accept_header" \ -H "$api_version_header" \ -H 'Content-Type: application/octet-stream' \ --data-binary @"$asset" \ "${upload_url}?name=${name}" >/dev/null done echo "Published GitHub release $version with ${#asset_files[@]} assets."