diff --git a/.gitea/workflows/sync-github-release-assets.yml b/.gitea/workflows/sync-github-release-assets.yml new file mode 100644 index 0000000..882d6f9 --- /dev/null +++ b/.gitea/workflows/sync-github-release-assets.yml @@ -0,0 +1,72 @@ +# Upstream GitHub (Win/mac CI artifacts): https://github.com/Sudo-Ivan/MeshChatX +# This Gitea project (releases + API uploads): https://git.quad4.io/RNS-Things/MeshChatX +# Secret GITEA_API_URL: API origin https://git.quad4.io (no trailing slash), same as .gitea/workflows/build.yml +# +# Pull Windows/macOS artifacts from GitHub Actions (.github/workflows/build-release.yml) and +# attach them to the Gitea release for the same tag (Linux assets come from build.yml). +# +# On GitHub: tag push runs build-release.yml; branch pushes to "dev" only run .github/workflows/build.yml +# (no Win/mac artifact upload). +# +# Repository secrets: +# GITEA_API_URL, GITEA_TOKEN — same as .gitea/workflows/build.yml +# GITHUB_SYNC_PAT — GitHub PAT: repo + actions:read (list/download workflow artifacts) +# +# Run after the Gitea "Build and Release" job has created the draft (or any release with that tag). +name: Sync GitHub Win/mac release assets + +on: + workflow_dispatch: + inputs: + tag: + description: "Tag matching GitHub and Gitea (e.g. v4.4.0)" + required: true + type: string + +permissions: + contents: read + +env: + GITHUB_SYNC_REPOSITORY: Sudo-Ivan/MeshChatX + GITEA_SYNC_REPOSITORY: RNS-Things/MeshChatX + GITEA_API_URL: ${{ secrets.GITEA_API_URL }} + +jobs: + sync: + name: Fetch GitHub artifacts and upload to Gitea release + runs-on: ubuntu-latest + steps: + - name: Clone repository + run: | + set -eu + SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" + REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" + if [ -z "$SERVER" ] || [ -z "$REPO" ]; then + echo "Set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 + exit 1 + fi + if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then + TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" + git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" + fi + git clone "${SERVER}/${REPO}.git" . + git checkout "${GITHUB_SHA}" + + - name: Sync from GitHub Actions + env: + TAG: ${{ inputs.tag }} + GITHUB_REPOSITORY: ${{ env.GITHUB_SYNC_REPOSITORY }} + GITHUB_PAT: ${{ secrets.GITHUB_SYNC_PAT }} + GITEA_REPOSITORY: ${{ env.GITEA_SYNC_REPOSITORY }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + set -eu + if [ -z "${GITHUB_PAT:-}" ]; then + echo "Configure secret GITHUB_SYNC_PAT." >&2 + exit 1 + fi + if [ -z "${GITEA_API_URL:-}" ]; then + echo "Configure secret GITEA_API_URL (https://git.quad4.io for RNS-Things/MeshChatX)." >&2 + exit 1 + fi + bash scripts/ci/sync-github-release-assets.sh diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 93686ac..77e4ae5 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,4 +1,5 @@ -# Tagged releases from master: Windows + macOS builds. +# Tagged releases: Windows + macOS builds (same idea as .gitea/workflows/build.yml — any tag +# points at a commit; no requirement that the tag be on master). # # Pinned first-party actions (bump tag and SHA together when upgrading): # actions/checkout@v6.0.1 8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -31,29 +32,8 @@ env: PNPM_VERSION: "10.32.1" jobs: - verify-master: - name: Verify tag on master - runs-on: ubuntu-latest - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') - steps: - - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - with: - fetch-depth: 0 - - - name: Ensure tagged commit is on master - run: | - set -euo pipefail - git fetch origin master - if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/master; then - echo "Tagged commit is not an ancestor of origin/master; release tags must be cut from master." >&2 - exit 1 - fi - build-release: name: Build release (${{ matrix.label }}) - needs: verify-master - if: always() && (needs.verify-master.result == 'success' || needs.verify-master.result == 'skipped') strategy: fail-fast: false matrix: diff --git a/scripts/ci/sync-github-release-assets.sh b/scripts/ci/sync-github-release-assets.sh new file mode 100644 index 0000000..e2a7c84 --- /dev/null +++ b/scripts/ci/sync-github-release-assets.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# Download Windows/macOS build artifacts from GitHub Actions (build-release workflow) +# and attach them to an existing Gitea release. Best-effort: missing platforms are skipped. +# +# Required env: TAG, GITHUB_REPOSITORY, GITHUB_PAT, GITEA_API_URL, GITEA_REPOSITORY, GITEA_TOKEN +set -euo pipefail + +TAG="${TAG:?set TAG to the release tag (e.g. v1.2.3)}" +GITHUB_REPOSITORY="${GITHUB_REPOSITORY:?}" +GITHUB_PAT="${GITHUB_PAT:?}" +GITEA_API_URL="${GITEA_API_URL:?}" +GITEA_REPOSITORY="${GITEA_REPOSITORY:?}" +GITEA_TOKEN="${GITEA_TOKEN:?}" + +GITEA_API_URL="${GITEA_API_URL%/}" +GH_API="https://api.github.com/repos/${GITHUB_REPOSITORY}" +AUTH_GH=(-H "Authorization: Bearer ${GITHUB_PAT}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28") +AUTH_GITEA=(-H "Authorization: token ${GITEA_TOKEN}" -H "Accept: application/json") + +WORKDIR=$(mktemp -d) +trap 'rm -rf "${WORKDIR}"' EXIT + +enc_tag() { + printf '%s' "$1" | jq -sRr @uri +} + +log() { + printf '%s\n' "$*" >&2 +} + +if ! command -v jq >/dev/null 2>&1; then + log "Error: jq is required." + exit 1 +fi + +COMMIT_SHA="" +if COMMIT_JSON=$(curl -sS -f "${AUTH_GH[@]}" "${GH_API}/commits/$(enc_tag "$TAG")" 2>/dev/null); then + COMMIT_SHA=$(printf '%s' "$COMMIT_JSON" | jq -r '.sha // empty') +fi + +TAG_ENC=$(enc_tag "$TAG") +RUNS_URL="${GH_API}/actions/workflows/build-release.yml/runs?event=push&branch=${TAG_ENC}&per_page=30" +RUN_ID="" +if RUNS_JSON=$(curl -sS -f "${AUTH_GH[@]}" "$RUNS_URL" 2>/dev/null); then + RUN_ID=$(printf '%s' "$RUNS_JSON" | jq -r ' + [.workflow_runs[] | select(.conclusion == "success")] + | sort_by(.created_at) | reverse | .[0].id // empty + ') +fi + +if [ -z "$RUN_ID" ] && [ -n "$COMMIT_SHA" ]; then + if RUNS_JSON=$(curl -sS -f "${AUTH_GH[@]}" "${GH_API}/actions/workflows/build-release.yml/runs?per_page=50" 2>/dev/null); then + RUN_ID=$(printf '%s' "$RUNS_JSON" | jq -r --arg sha "$COMMIT_SHA" ' + [.workflow_runs[] | select(.head_sha == $sha and .conclusion == "success")] + | sort_by(.created_at) | reverse | .[0].id // empty + ') + fi +fi + +if [ -z "$RUN_ID" ]; then + log "No successful GitHub Actions run found for build-release.yml (tag=${TAG}). Skipping GitHub artifact sync." + exit 0 +fi + +log "Using GitHub workflow run id=${RUN_ID} for tag ${TAG}" + +ART_JSON=$(curl -sS "${AUTH_GH[@]}" "${GH_API}/actions/runs/${RUN_ID}/artifacts?per_page=100" || true) +N=$(printf '%s' "${ART_JSON:-{}}" | jq -r '(.artifacts // []) | length') +if [ "${N:-0}" -eq 0 ]; then + log "No artifacts on GitHub run ${RUN_ID} (or API error). Nothing to download." + exit 0 +fi + +STAGE="${WORKDIR}/stage" +mkdir -p "$STAGE" + +printf '%s' "$ART_JSON" | jq -r '.artifacts[] | "\(.id)|\(.name)|\(.archive_download_url)"' | while IFS='|' read -r _art_id art_name dl_url; do + case "$art_name" in + meshchatx-windows-*|meshchatx-macos-*) ;; + *) + log "Skipping artifact with unexpected name: ${art_name}" + continue + ;; + esac + ZIP="${WORKDIR}/$(echo "$art_name" | tr '/' '_').zip" + log "Downloading ${art_name}..." + if ! curl -sS -fL "${AUTH_GH[@]}" -o "$ZIP" "$dl_url"; then + log "Warning: failed to download ${art_name}; continuing." + continue + fi + EX="${WORKDIR}/ex-${art_name}" + mkdir -p "$EX" + if ! unzip -q -o "$ZIP" -d "$EX" 2>/dev/null; then + log "Warning: unzip failed for ${art_name}; continuing." + continue + fi + find "$EX" -type f \( -name '*.exe' -o -name '*.dmg' -o -name '*.blockmap' -o -name '*.yml' -o -name '*.yaml' \) -print0 2>/dev/null \ + | while IFS= read -r -d '' f; do + base=$(basename "$f") + cp -f "$f" "${STAGE}/${base}" + log "Staged ${base}" + done +done + +STAGED_N=$(find "$STAGE" -mindepth 1 -maxdepth 1 -type f 2>/dev/null | wc -l) +STAGED_N=${STAGED_N//[[:space:]]/} +if [ "${STAGED_N:-0}" -eq 0 ]; then + log "No .exe/.dmg (or related) files extracted from GitHub artifacts. Nothing to upload." + exit 0 +fi + +REL_JSON=$(curl -sS "${AUTH_GITEA[@]}" "${GITEA_API_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/tags/${TAG}") +REL_ID=$(printf '%s' "$REL_JSON" | jq -r '.id // empty') +if [ -z "$REL_ID" ] || [ "$REL_ID" = "null" ]; then + log "Error: No Gitea release for tag '${TAG}'. Create the release first (e.g. push the tag so .gitea/workflows/build.yml runs)." + exit 1 +fi + +log "Uploading to Gitea release id=${REL_ID} (${GITEA_REPOSITORY}@${TAG})" + +find "$STAGE" -maxdepth 1 -type f -print0 | while IFS= read -r -d '' f; do + base=$(basename "$f") + NAME_ENC=$(printf '%s' "$base" | jq -sRr @uri) + if curl -sS -f "${AUTH_GITEA[@]}" -F "attachment=@${f}" \ + "${GITEA_API_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${REL_ID}/assets?name=${NAME_ENC}" >/dev/null; then + log "Uploaded ${base}" + else + log "Warning: upload failed or duplicate for ${base}; continuing." + fi +done + +log "Done."