name: Forgejo Release Builder # NOTE: keep Forgejo action refs node20-compatible for runner v3.5.1. on: push: tags: - v* workflow_dispatch: inputs: tag: description: 'Release tag to build and publish (e.g. v1.13.6). Must already exist on the remote.' required: true type: string permissions: contents: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: release: runs-on: desktop-release steps: - name: Clone repository shell: bash env: FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} DISPATCH_TAG: ${{ github.event.inputs.tag }} run: | set -euo pipefail rm -rf .git git init . origin_url="${GITHUB_SERVER_URL#https://}" origin_url="${origin_url#http://}" git remote add origin "https://copilot:${FORGEJO_TOKEN}@${origin_url}/${GITHUB_REPOSITORY}.git" if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then git -c http.sslVerify=false fetch --tags --prune origin "refs/tags/${DISPATCH_TAG}" else git -c http.sslVerify=false fetch --tags --prune origin "${GITHUB_SHA}" fi git checkout --force FETCH_HEAD - name: Set up JDK shell: bash run: | set -euo pipefail pkg_install() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else echo "Need root or sudo to install packages." exit 1 fi } if command -v java >/dev/null 2>&1; then java -version else echo "PATH=$PATH" echo "pkg managers: dnf=$(command -v dnf || true) apt=$(command -v apt-get || true) apk=$(command -v apk || true) pacman=$(command -v pacman || true)" if [ -x /usr/bin/dnf ] || command -v dnf >/dev/null 2>&1; then pkg_install dnf install -y java-17-openjdk-devel elif [ -x /usr/bin/apt-get ] || command -v apt-get >/dev/null 2>&1; then pkg_install apt-get update pkg_install apt-get install -y openjdk-17-jdk elif [ -x /sbin/apk ] || [ -x /usr/sbin/apk ] || command -v apk >/dev/null 2>&1; then pkg_install apk add --no-cache openjdk17 elif [ -x /usr/bin/pacman ] || command -v pacman >/dev/null 2>&1; then pkg_install pacman -Sy --noconfirm jdk17-openjdk else echo "No supported package manager found for JDK installation." exit 1 fi java -version fi if command -v javac >/dev/null 2>&1; then javac -version java_home="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" echo "JAVA_HOME=$java_home" >> "$GITHUB_ENV" fi - name: Configure persistent Gradle cache shell: bash run: | set -euo pipefail gradle_home="${KOMIKKU_GRADLE_CACHE:-${HOME}/.cache/komikku-ci-gradle}" mkdir -p "${gradle_home}" echo "GRADLE_USER_HOME=${gradle_home}" >> "${GITHUB_ENV}" echo "Gradle dependencies/cache: ${gradle_home}" - name: Prepare release metadata id: prepare shell: bash run: | set -euo pipefail if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then version_tag="${{ github.event.inputs.tag }}" else version_tag="${GITHUB_REF_NAME:-${GITHUB_REF#refs/tags/}}" fi version_display="$(printf '%s' "$version_tag" | sed -E 's/^v//' )" previous_tag="$(git tag --list 'v*' --sort=-version:refname | grep -Fxv "$version_tag" | head -n 1 || true)" upstream_repo="komikku-app/komikku" upstream_ref="" upstream_base="" for ref in master main; do candidate="$(git ls-remote "https://github.com/${upstream_repo}.git" "refs/heads/${ref}" | awk '{print $1}' || true)" if [ -n "$candidate" ]; then upstream_ref="$ref" upstream_base="$candidate" break fi done echo "VERSION_TAG=$version_tag" >> "$GITHUB_OUTPUT" echo "VERSION_DISPLAY=$version_display" >> "$GITHUB_OUTPUT" echo "PREV_TAG_NAME=$previous_tag" >> "$GITHUB_OUTPUT" echo "UPSTREAM_REPO=$upstream_repo" >> "$GITHUB_OUTPUT" echo "UPSTREAM_REF=$upstream_ref" >> "$GITHUB_OUTPUT" echo "UPSTREAM_BASE=$upstream_base" >> "$GITHUB_OUTPUT" if [ -n "$upstream_base" ]; then git fetch --no-tags --depth=1 "https://github.com/${upstream_repo}.git" "$upstream_base" || true fi # Release notes: only commits since the *previous release tag* (short, correct scope). # Do not use upstream_base..HEAD here — that lists the whole fork delta and is huge. changelog_commit_limit=50 log_range="" if [ -n "$previous_tag" ]; then log_range="${previous_tag}..HEAD" fi commit_log_file="$(mktemp /tmp/komikku-commit-logs.XXXXXX)" declare -a new_lines fix_lines improve_lines other_lines bucket_for_subject() { local s="$1" local b=other shopt -s nocasematch local r_feat r_feature r_add r_fix r_perf r_refactor r_chore r_docs r_style r_test r_ci r_build r_improve r_feat='^feat\([^)]*\):' r_feature='^feature\([^)]*\):' r_add='^add\([^)]*\):' r_fix='^fix\([^)]*\):' r_perf='^perf\([^)]*\):' r_refactor='^refactor\([^)]*\):' r_chore='^chore\([^)]*\):' r_docs='^docs\([^)]*\):' r_style='^style\([^)]*\):' r_test='^test\([^)]*\):' r_ci='^ci\([^)]*\):' r_build='^build\([^)]*\):' r_improve='^improve\([^)]*\):' if [[ "$s" =~ ^feat: ]] || [[ "$s" =~ $r_feat ]]; then b=new elif [[ "$s" =~ ^feature: ]] || [[ "$s" =~ $r_feature ]]; then b=new elif [[ "$s" =~ ^add: ]] || [[ "$s" =~ $r_add ]]; then b=new elif [[ "$s" =~ ^fix: ]] || [[ "$s" =~ $r_fix ]]; then b=fix elif [[ "$s" =~ ^perf: ]] || [[ "$s" =~ $r_perf ]]; then b=improve elif [[ "$s" =~ ^refactor: ]] || [[ "$s" =~ $r_refactor ]]; then b=improve elif [[ "$s" =~ ^chore: ]] || [[ "$s" =~ $r_chore ]]; then b=improve elif [[ "$s" =~ ^docs: ]] || [[ "$s" =~ $r_docs ]]; then b=improve elif [[ "$s" =~ ^style: ]] || [[ "$s" =~ $r_style ]]; then b=improve elif [[ "$s" =~ ^test: ]] || [[ "$s" =~ $r_test ]]; then b=improve elif [[ "$s" =~ ^ci: ]] || [[ "$s" =~ $r_ci ]]; then b=improve elif [[ "$s" =~ ^build: ]] || [[ "$s" =~ $r_build ]]; then b=improve elif [[ "$s" =~ ^improve: ]] || [[ "$s" =~ $r_improve ]]; then b=improve fi shopt -u nocasematch printf '%s' "$b" } clear_buckets() { new_lines=() fix_lines=() improve_lines=() other_lines=() } fill_buckets_from_git() { local rev_range="$1" shift while IFS= read -r line || [ -n "${line:-}" ]; do [ -z "${line:-}" ] && continue subject="${line%%$'\x1f'*}" author="${line#*$'\x1f'}" entry="- ${subject} (@${author})" case "$(bucket_for_subject "$subject")" in new) new_lines+=("$entry") ;; fix) fix_lines+=("$entry") ;; improve) improve_lines+=("$entry") ;; *) other_lines+=("$entry") ;; esac done < <(git log "$@" --pretty=tformat:'%s%x1f%an' "$rev_range") } emit_buckets_from_arrays() { local any=0 if [ ${#new_lines[@]} -gt 0 ]; then printf '%s\n' "##### New" "" printf '%s\n' "${new_lines[@]}" printf '\n' any=1 fi if [ ${#fix_lines[@]} -gt 0 ]; then printf '%s\n' "##### Fix" "" printf '%s\n' "${fix_lines[@]}" printf '\n' any=1 fi if [ ${#improve_lines[@]} -gt 0 ]; then printf '%s\n' "##### Improve" "" printf '%s\n' "${improve_lines[@]}" printf '\n' any=1 fi if [ ${#other_lines[@]} -gt 0 ]; then printf '%s\n' "##### Other" "" printf '%s\n' "${other_lines[@]}" printf '\n' any=1 fi [ "$any" -eq 1 ] } if [ -n "$log_range" ]; then total_in_range="$(git rev-list --count "${log_range}" 2>/dev/null || printf '0')" clear_buckets fill_buckets_from_git "$log_range" "-n${changelog_commit_limit}" { if emit_buckets_from_arrays; then if [ "${total_in_range}" -gt "${changelog_commit_limit}" ] 2>/dev/null; then printf '%s\n' "" "_Showing the ${changelog_commit_limit} most recent commits since ${previous_tag}._" \ "_Older commits in this release: use **Full Changelog** below._" "" fi else printf '%s\n' "##### Other" "" "- No commit messages in this tag range." "" fi } > "${commit_log_file}" else printf '%s\n' "##### Other" "" "- First release tag, or no prior \`v*\` tag found — see repository history." "" > "${commit_log_file}" fi # Do not pass multiline commit text via step outputs / env (breaks some Forgejo/act_runner # runners before the shell starts — empty logs). Only pass a temp file path. echo "COMMIT_LOG_FILE=${commit_log_file}" >> "${GITHUB_ENV}" - name: Set up Gradle shell: bash run: | set -euo pipefail sdk_candidates=( "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}" "/opt/android-sdk-linux" "/home/yuna/Android/Sdk" "/home/runner/Android/Sdk" "/root/Android/Sdk" ) sdk_dir="" for candidate in "${sdk_candidates[@]}"; do if [ -n "$candidate" ] && [ -d "$candidate/platforms" ] && [ -d "$candidate/build-tools" ]; then sdk_dir="$candidate" break fi done if [ -z "$sdk_dir" ]; then echo "Android SDK not found. Checked:" for candidate in "${sdk_candidates[@]}"; do [ -n "$candidate" ] && echo " - $candidate" done exit 1 fi export ANDROID_HOME="$sdk_dir" export ANDROID_SDK_ROOT="$sdk_dir" echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV" echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV" sdkmanager_bin="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" if [ -x "$sdkmanager_bin" ]; then if [ ! -x "$ANDROID_HOME/build-tools/35.0.0/aidl" ]; then echo "Installing Android SDK components via sdkmanager..." set +o pipefail yes | "$sdkmanager_bin" --licenses >/dev/null || true yes | "$sdkmanager_bin" "platform-tools" "build-tools;35.0.0" set -o pipefail else echo "Android build-tools 35.0.0 present; skipping sdkmanager (saves time)." fi else echo "sdkmanager not found at $sdkmanager_bin" fi if [ ! -x "$ANDROID_HOME/build-tools/35.0.0/aidl" ]; then echo "Expected executable missing: $ANDROID_HOME/build-tools/35.0.0/aidl" ls -la "$ANDROID_HOME/build-tools" || true exit 1 fi printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties chmod +x ./gradlew ./gradlew --no-daemon --max-workers=1 \ "-Dorg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:ReservedCodeCacheSize=256m -Dfile.encoding=UTF-8" \ -Dorg.gradle.parallel=false \ -Dorg.gradle.configureondemand=false \ -Dorg.gradle.vfs.watch=false \ -Dkotlin.daemon.enabled=false \ "-Dkotlin.daemon.jvmargs=-Xmx2048m,-XX:MaxMetaspaceSize=512m,-XX:ReservedCodeCacheSize=256m" \ -Dkotlin.compiler.execution.strategy=in-process \ -Pkotlin.incremental=true \ --version - name: Build app run: | if [ -n "${ANDROID_HOME:-}" ]; then printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties fi ./gradlew --no-daemon --max-workers=1 \ "-Dorg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:ReservedCodeCacheSize=256m -Dfile.encoding=UTF-8" \ -Dorg.gradle.parallel=false \ -Dorg.gradle.configureondemand=false \ -Dorg.gradle.vfs.watch=false \ -Dkotlin.daemon.enabled=false \ "-Dkotlin.daemon.jvmargs=-Xmx2048m,-XX:MaxMetaspaceSize=512m,-XX:ReservedCodeCacheSize=256m" \ -Dkotlin.compiler.execution.strategy=in-process \ -Pkotlin.incremental=true \ --stacktrace \ assembleRelease -Penable-updater -Pdisable-code-shrink - name: Rename release artifacts shell: bash run: | set -euo pipefail version_tag="${{ steps.prepare.outputs.VERSION_TAG }}" mv app/build/outputs/apk/release/app-universal-release-unsigned.apk "Komikku-${version_tag}-unsigned.apk" mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk "Komikku-arm64-v8a-${version_tag}-unsigned.apk" mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk "Komikku-armeabi-v7a-${version_tag}-unsigned.apk" mv app/build/outputs/apk/release/app-x86-release-unsigned.apk "Komikku-x86-${version_tag}-unsigned.apk" mv app/build/outputs/apk/release/app-x86_64-release-unsigned.apk "Komikku-x86_64-${version_tag}-unsigned.apk" if [ -d app/build/outputs/mapping/release ]; then zip -qr "Komikku-mapping-${version_tag}.zip" app/build/outputs/mapping/release fi - name: Sign APKs shell: bash env: SIGNING_KEYSTORE_B64: ${{ secrets.SIGNING_KEYSTORE_B64 }} SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} run: | set -euo pipefail if [ -z "${SIGNING_KEYSTORE_B64}" ] || [ -z "${SIGNING_KEYSTORE_PASSWORD}" ] || [ -z "${SIGNING_KEY_ALIAS}" ] || [ -z "${SIGNING_KEY_PASSWORD}" ]; then echo "Signing secrets are required for installable release APKs." echo "Set SIGNING_KEYSTORE_B64, SIGNING_KEYSTORE_PASSWORD, SIGNING_KEY_ALIAS, SIGNING_KEY_PASSWORD." exit 1 fi keystore_file="$(mktemp)" printf '%s' "${SIGNING_KEYSTORE_B64}" | base64 -d > "${keystore_file}" apksigner_bin="" if command -v apksigner >/dev/null 2>&1; then apksigner_bin="$(command -v apksigner)" elif [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/build-tools" ]; then apksigner_bin="$(ls -1 "${ANDROID_HOME}"/build-tools/*/apksigner 2>/dev/null | sort -V | tail -n 1 || true)" fi if [ -z "${apksigner_bin}" ]; then echo "apksigner not found in PATH or ANDROID_HOME/build-tools." exit 1 fi for unsigned_apk in Komikku-*-unsigned.apk; do [ -e "${unsigned_apk}" ] || continue signed_apk="${unsigned_apk/-unsigned.apk/.apk}" "${apksigner_bin}" sign \ --ks "${keystore_file}" \ --ks-key-alias "${SIGNING_KEY_ALIAS}" \ --ks-pass "pass:${SIGNING_KEYSTORE_PASSWORD}" \ --key-pass "pass:${SIGNING_KEY_PASSWORD}" \ --out "${signed_apk}" \ "${unsigned_apk}" "${apksigner_bin}" verify "${signed_apk}" done # Catch corrupt / non-APK artifacts before upload (e.g. truncated build output). Same # manifest parse path the installer uses for "Paket konnte nicht geparst werden". aapt_bin="" if [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/build-tools" ]; then aapt_bin="$(ls -1 "${ANDROID_HOME}"/build-tools/*/aapt 2>/dev/null | sort -V | tail -n 1 || true)" fi if [ -z "${aapt_bin}" ] || [ ! -x "${aapt_bin}" ]; then echo "aapt not found under ANDROID_HOME; cannot verify APK manifests." exit 1 fi for signed_apk in Komikku-*.apk; do case "${signed_apk}" in *-unsigned.apk) continue ;; esac echo "Verifying installable APK: ${signed_apk}" if ! unzip -tqq "${signed_apk}" >/dev/null 2>&1; then echo "ERROR: ${signed_apk} is not a valid zip (truncated or corrupt build output?)." exit 1 fi first_line="$("${aapt_bin}" dump badging "${signed_apk}" 2>/dev/null | head -n 1 || true)" if ! printf '%s\n' "${first_line}" | grep -q '^package:'; then echo "ERROR: ${signed_apk} failed manifest parse (first line: ${first_line:-})." echo "First 64 bytes (expect PK zip header 504b0304):" head -c 64 "${signed_apk}" | hexdump -C || true exit 1 fi sha256sum "${signed_apk}" done - name: Create Obtainium-friendly aliases shell: bash run: | set -euo pipefail alias_asset() { src="$1" dst="$2" if [ -e "$src" ]; then cp -f "$src" "$dst" fi } # Only publish signed APK aliases. alias_asset "$(ls -1t Komikku-arm64-v8a-*.apk 2>/dev/null | head -n1)" "Komikku-arm64-v8a.apk" for candidate in Komikku-*.apk; do case "$candidate" in *arm64-v8a*|*armeabi-v7a*|*x86_64*|*x86*|*-unsigned.apk) ;; *) alias_asset "$candidate" "Komikku-universal.apk" break ;; esac done - name: Create or update release id: release shell: bash env: FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION_TAG: ${{ steps.prepare.outputs.VERSION_TAG }} VERSION_DISPLAY: ${{ steps.prepare.outputs.VERSION_DISPLAY }} PREV_TAG_NAME: ${{ steps.prepare.outputs.PREV_TAG_NAME }} REPO_URL: ${{ github.server_url }}/${{ github.repository }} REPOSITORY_NAME: ${{ github.repository }} FORGEJO_CURL_INSECURE: "true" run: | set -euo pipefail echo "Create or update release: shell running; COMMIT_LOG_FILE=${COMMIT_LOG_FILE:-}" export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" discover_jq() { if command -v jq >/dev/null 2>&1; then JQ_BIN="$(command -v jq)" export JQ_BIN return 0 fi for cand in /usr/bin/jq /usr/local/bin/jq /bin/jq; do if [ -x "$cand" ]; then JQ_BIN="$cand" export JQ_BIN return 0 fi done return 1 } ensure_jq() { if discover_jq; then return 0 fi run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else return 1 fi } if command -v dnf >/dev/null 2>&1; then run_as_root dnf install -y jq elif command -v apt-get >/dev/null 2>&1; then run_as_root apt-get update run_as_root apt-get install -y jq elif command -v apk >/dev/null 2>&1; then run_as_root apk add --no-cache jq elif command -v pacman >/dev/null 2>&1; then run_as_root pacman -Sy --noconfirm jq fi discover_jq } ensure_curl() { if command -v curl >/dev/null 2>&1; then return 0 fi run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else return 1 fi } if command -v dnf >/dev/null 2>&1; then run_as_root dnf install -y curl elif command -v apt-get >/dev/null 2>&1; then run_as_root apt-get update run_as_root apt-get install -y curl elif command -v apk >/dev/null 2>&1; then run_as_root apk add --no-cache curl elif command -v pacman >/dev/null 2>&1; then run_as_root pacman -Sy --noconfirm curl fi command -v curl >/dev/null 2>&1 } ensure_jq || { echo "jq not found; install jq on the runner (dnf install -y jq)."; exit 1; } ensure_curl || { echo "curl not found; install curl on the runner (dnf install -y curl)."; exit 1; } curl_tls_args=() if [ "${FORGEJO_CURL_INSECURE:-false}" = "true" ]; then curl_tls_args+=(--insecure) fi api_base="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" payload_file="$(mktemp)" response_file="$(mktemp)" commit_logs="" if [ -n "${COMMIT_LOG_FILE:-}" ] && [ -f "${COMMIT_LOG_FILE}" ]; then commit_logs="$(cat "${COMMIT_LOG_FILE}")" fi commit_logs="$(printf '%s' "$commit_logs" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" if [ -z "$commit_logs" ]; then commit_logs=$'##### Other\n\n- Initial release\n' fi full_changelog="" if [ -n "${PREV_TAG_NAME:-}" ]; then full_changelog="**Full Changelog**: [${REPOSITORY_NAME}@${PREV_TAG_NAME}...${VERSION_TAG}](${REPO_URL}/compare/${PREV_TAG_NAME}...${VERSION_TAG})" fi release_body_file="$(mktemp)" { printf '%s\n' \ "#### What's Changed" \ "" \ "${commit_logs}" \ "" if [ -n "${full_changelog}" ]; then printf '%s\n' "${full_changelog}" "" fi printf '%s\n' \ "" \ "> [!TIP]" \ ">" \ "> ### If you are unsure which version to download then go with \`Komikku-universal.apk\`" } > "${release_body_file}" # Build JSON — body from file only (jq --arg body "$huge" hits ARG_MAX). "${JQ_BIN}" -n \ --arg tag "${VERSION_TAG}" \ --arg title "Komikku ${VERSION_DISPLAY}" \ --rawfile body "${release_body_file}" \ '{tag_name: $tag, name: $title, body: $body, draft: false, prerelease: false}' > "${payload_file}" tag_path_enc="$("${JQ_BIN}" -nr --arg t "${VERSION_TAG}" '$t|@uri')" tag_get_url="${api_base}/tags/${tag_path_enc}" echo "Forgejo release API: GET ${tag_get_url}" status="$(curl "${curl_tls_args[@]}" -sS -o "$response_file" -w '%{http_code}' \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${tag_get_url}")" echo "release-by-tag GET HTTP status: ${status}" if [ "${status}" != "200" ] && [ "${status}" != "404" ]; then echo "Unexpected GET response body:" cat "$response_file" exit 1 fi release_id="" if [ "${status}" = "200" ]; then release_id="$("${JQ_BIN}" -r '.id // empty' < "$response_file")" if [ -z "${release_id}" ] || [ "${release_id}" = "null" ]; then echo "GET returned 200 but missing release id; body:" cat "$response_file" exit 1 fi echo "PATCH existing release id=${release_id}" patch_status="$(curl "${curl_tls_args[@]}" -sS -o "$response_file" -w '%{http_code}' \ -X PATCH \ -H "Authorization: token ${FORGEJO_TOKEN}" \ -H 'Content-Type: application/json' \ --data @"$payload_file" \ "${api_base}/${release_id}")" echo "PATCH HTTP status: ${patch_status}" if [ "${patch_status}" != "200" ]; then echo "PATCH response body:" cat "$response_file" exit 1 fi elif [ "${status}" = "404" ]; then echo "POST new release for tag ${VERSION_TAG}" post_status="$(curl "${curl_tls_args[@]}" -sS -o "$response_file" -w '%{http_code}' \ -X POST \ -H "Authorization: token ${FORGEJO_TOKEN}" \ -H 'Content-Type: application/json' \ --data @"$payload_file" \ "${api_base}")" echo "POST HTTP status: ${post_status}" if [ "${post_status}" != "201" ] && [ "${post_status}" != "200" ]; then echo "POST response body:" cat "$response_file" exit 1 fi release_id="$("${JQ_BIN}" -r '.id // empty' < "$response_file")" if [ -z "${release_id}" ] || [ "${release_id}" = "null" ]; then echo "POST succeeded but missing release id; body:" cat "$response_file" exit 1 fi fi echo "RELEASE_ID=${release_id}" >> "$GITHUB_OUTPUT" - name: Upload release assets shell: bash env: FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_ID: ${{ steps.release.outputs.RELEASE_ID }} FORGEJO_CURL_INSECURE: "true" run: | set -euo pipefail export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" discover_jq() { if command -v jq >/dev/null 2>&1; then JQ_BIN="$(command -v jq)" export JQ_BIN return 0 fi for cand in /usr/bin/jq /usr/local/bin/jq /bin/jq; do if [ -x "$cand" ]; then JQ_BIN="$cand" export JQ_BIN return 0 fi done return 1 } ensure_jq() { if discover_jq; then return 0 fi run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else return 1 fi } if command -v dnf >/dev/null 2>&1; then run_as_root dnf install -y jq elif command -v apt-get >/dev/null 2>&1; then run_as_root apt-get update run_as_root apt-get install -y jq elif command -v apk >/dev/null 2>&1; then run_as_root apk add --no-cache jq elif command -v pacman >/dev/null 2>&1; then run_as_root pacman -Sy --noconfirm jq fi discover_jq } ensure_curl() { if command -v curl >/dev/null 2>&1; then return 0 fi run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else return 1 fi } if command -v dnf >/dev/null 2>&1; then run_as_root dnf install -y curl elif command -v apt-get >/dev/null 2>&1; then run_as_root apt-get update run_as_root apt-get install -y curl elif command -v apk >/dev/null 2>&1; then run_as_root apk add --no-cache curl elif command -v pacman >/dev/null 2>&1; then run_as_root pacman -Sy --noconfirm curl fi command -v curl >/dev/null 2>&1 } ensure_jq || { echo "jq not found; install jq on the runner (dnf install -y jq)."; exit 1; } ensure_curl || { echo "curl not found; install curl on the runner (dnf install -y curl)."; exit 1; } curl_tls_args=() if [ "${FORGEJO_CURL_INSECURE:-false}" = "true" ]; then curl_tls_args+=(--insecure) fi if [ -z "${RELEASE_ID:-}" ] || [ "${RELEASE_ID}" = "null" ]; then echo "RELEASE_ID missing or null — create/update release step likely failed." exit 1 fi release_api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" assets_file="$(mktemp)" err_body="$(mktemp)" assets_status="$(curl "${curl_tls_args[@]}" -sS -o "$assets_file" -w '%{http_code}' \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${release_api}/assets")" if [ "${assets_status}" != "200" ]; then echo "List assets failed HTTP ${assets_status}:" cat "$assets_file" rm -f "$assets_file" "$err_body" exit 1 fi for file in Komikku-*.apk Komikku-mapping-*.zip; do [ -e "$file" ] || continue if [ "${file#*-unsigned.apk}" != "${file}" ]; then continue fi asset_name="$(basename "$file")" asset_id="$("${JQ_BIN}" -r --arg n "${asset_name}" '(.[] | select(.name == $n) | .id) // empty' "$assets_file")" if [ -n "$asset_id" ] && [ "$asset_id" != "null" ]; then del_status="$(curl "${curl_tls_args[@]}" -sS -o "$err_body" -w '%{http_code}' \ -X DELETE \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${release_api}/assets/${asset_id}")" if [ "${del_status}" != "204" ] && [ "${del_status}" != "200" ] && [ "${del_status}" != "404" ]; then echo "DELETE old asset ${asset_name} failed HTTP ${del_status}:" cat "$err_body" rm -f "$assets_file" "$err_body" exit 1 fi fi encoded_name="$("${JQ_BIN}" -nr --arg s "${asset_name}" '$s | @uri')" up_status="$(curl "${curl_tls_args[@]}" -sS -o "$err_body" -w '%{http_code}' \ -X POST \ -H "Authorization: token ${FORGEJO_TOKEN}" \ -H 'Content-Type: application/octet-stream' \ --data-binary @"$file" \ "${release_api}/assets?name=${encoded_name}")" if [ "${up_status}" != "201" ] && [ "${up_status}" != "200" ]; then echo "Upload ${asset_name} failed HTTP ${up_status}:" cat "$err_body" rm -f "$assets_file" "$err_body" exit 1 fi echo "Uploaded ${asset_name} (HTTP ${up_status})" done rm -f "$assets_file" "$err_body"