name: Release # Gitea Actions workflow: # - pushes to main build the Windows GNU package and publish a build-date Release. # - tag pushes (v*) build the Windows GNU package and publish the matching Release. # - manual runs build the same package and publish a build-date Release. on: push: branches: ["main"] tags: ["v*"] workflow_dispatch: env: HTTP_PROXY: "http://10.31.88.88:7891" HTTPS_PROXY: "http://10.31.88.88:7891" NO_PROXY: "localhost,127.0.0.1,.local,*.local" permissions: contents: write jobs: build-windows-gnu: name: windows-x86_64-gnu runs-on: Ubuntu_docker_act env: TARGET: x86_64-pc-windows-gnu BIN: meatshell.exe CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc CARGO_TARGET_X86_64_PC_WINDOWS_GNU_AR: x86_64-w64-mingw32-ar CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc CXX_x86_64_pc_windows_gnu: x86_64-w64-mingw32-g++ AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar WINDRES: x86_64-w64-mingw32-windres PKG_CONFIG_ALLOW_CROSS: "1" steps: - name: 显示代理配置 shell: bash run: | echo "=== Proxy Configuration ===" echo "HTTP_PROXY: ${HTTP_PROXY}" echo "HTTPS_PROXY: ${HTTPS_PROXY}" echo "NO_PROXY: ${NO_PROXY}" echo "==========================" - name: 测试代理连通性 shell: bash run: | echo "Testing proxy connectivity..." if curl -x "${HTTP_PROXY}" -I --connect-timeout 10 https://google.com 2>&1 | grep -q "HTTP"; then echo "✅ Proxy is working!" else echo "⚠️ Proxy test failed, but continuing..." fi - name: 配置 Git 全局代理 shell: bash run: | git config --global http.proxy ${HTTP_PROXY} git config --global https.proxy ${HTTPS_PROXY} git config --global http.sslVerify false git config --global https.sslVerify false echo "✅ Git proxy configured" - name: 配置 Cargo 代理 shell: bash run: | mkdir -p ~/.cargo printf '[http]\n' > ~/.cargo/config.toml printf 'proxy = "%s"\n' "${HTTP_PROXY}" >> ~/.cargo/config.toml printf 'check-revoke = false\n\n' >> ~/.cargo/config.toml printf '[https]\n' >> ~/.cargo/config.toml printf 'proxy = "%s"\n' "${HTTPS_PROXY}" >> ~/.cargo/config.toml printf 'check-revoke = false\n\n' >> ~/.cargo/config.toml printf '[net]\n' >> ~/.cargo/config.toml printf 'git-fetch-with-cli = true\n' >> ~/.cargo/config.toml printf 'retry = 10\n' >> ~/.cargo/config.toml echo "✅ Cargo proxy configured" cat ~/.cargo/config.toml - name: 配置 Cargo 环境变量 shell: bash run: | echo "CARGO_HTTP_PROXY=${HTTP_PROXY}" >> $GITHUB_ENV echo "CARGO_HTTPS_PROXY=${HTTPS_PROXY}" >> $GITHUB_ENV echo "CARGO_HTTP_SSL_VERIFY=false" >> $GITHUB_ENV echo "CARGO_HTTPS_SSL_VERIFY=false" >> $GITHUB_ENV - name: 配置 curl 默认代理 shell: bash run: | mkdir -p ~/.curl printf 'proxy = %s\n' "${HTTP_PROXY}" > ~/.curlrc printf 'insecure\n' >> ~/.curlrc echo "✅ curl proxy configured" - uses: actions/checkout@v4 with: clean: false - name: Cache Cargo registry and target uses: actions/cache@v3 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-${{ env.TARGET }}-cargo-${{ hashFiles('Cargo.lock') }} restore-keys: | ${{ runner.os }}-${{ env.TARGET }}-cargo- ${{ runner.os }}-cargo- - name: Configure local Cargo cache shell: bash run: | set -eu CACHE_ROOT="${MEATSHELL_CI_CACHE:-$PWD/.cache/ci}" case "$CACHE_ROOT" in /*) ;; *) CACHE_ROOT="$PWD/$CACHE_ROOT" ;; esac mkdir -p "$CACHE_ROOT/cargo" "$CACHE_ROOT/target" echo "CARGO_HOME=$CACHE_ROOT/cargo" >> "$GITHUB_ENV" echo "CARGO_TARGET_DIR=$CACHE_ROOT/target" >> "$GITHUB_ENV" echo "CARGO_NET_RETRY=10" >> "$GITHUB_ENV" echo "Using Cargo cache root: $CACHE_ROOT" if [ -d "$CACHE_ROOT/target/$TARGET/release" ]; then echo "✅ Cache hit! Previous build artifacts found." echo "Release binary size: $(du -sh "$CACHE_ROOT/target/$TARGET/release/$BIN" 2>/dev/null | cut -f1 || echo 'not built yet')" else echo "❌ Cache miss or first run. Will perform full build." fi - name: 清理可能损坏的缓存 shell: bash run: | if [ -d ~/.cargo/registry/index/github.com-* ]; then echo "Cleaning corrupted cargo registry cache..." rm -rf ~/.cargo/registry/index/github.com-* fi - name: Show toolchain shell: bash run: | set -eu rustc --version cargo --version x86_64-w64-mingw32-gcc --version | head -n 1 x86_64-w64-mingw32-windres --version | head -n 1 - name: Ensure Rust target shell: bash run: rustup target add "$TARGET" - name: Update crates.io index with retry shell: bash run: | for i in 1 2 3 4 5; do echo "Attempt $i to fetch dependencies..." if cargo fetch 2>&1; then echo "✅ Cargo fetch succeeded on attempt $i" break fi if [ $i -eq 5 ]; then echo "❌ Cargo fetch failed after 5 attempts" exit 1 fi echo "Waiting 10 seconds before retry..." sleep 10 done - name: UI terminal smoke test shell: bash run: | set -eu BUILD_DIR="${CARGO_TARGET_DIR:-target}" cargo build rm -f "$BUILD_DIR/debug/meatshell-debug.log" meatshell-debug.log SMOKE_OUTPUT="$BUILD_DIR/debug/meatshell-smoke.out" set +e MEATSHELL_SMOKE_TERMINAL=1 timeout 8s xvfb-run -a "$BUILD_DIR/debug/meatshell" >"$SMOKE_OUTPUT" 2>&1 code=$? set -e if [ "$code" -ne 0 ]; then cat "$SMOKE_OUTPUT" if [ -f "$BUILD_DIR/debug/meatshell-debug.log" ]; then cat "$BUILD_DIR/debug/meatshell-debug.log" fi exit "$code" fi if [ -f "$BUILD_DIR/debug/meatshell-debug.log" ]; then if grep -E "ERROR|panic|Recursion detected" "$BUILD_DIR/debug/meatshell-debug.log"; then cat "$SMOKE_OUTPUT" cat "$BUILD_DIR/debug/meatshell-debug.log" exit 1 fi fi - name: Build Windows GNU release shell: bash run: | set -eu if command -v build-win-gnu >/dev/null 2>&1; then build-win-gnu else cargo build --release --target "$TARGET" fi - name: Package Windows zip shell: bash env: EVENT_NAME: ${{ gitea.event_name }} REF: ${{ gitea.ref }} REF_NAME: ${{ gitea.ref_name }} run: | set -eu BUILD_DIR="${CARGO_TARGET_DIR:-target}" event_name="${EVENT_NAME:-${GITHUB_EVENT_NAME:-}}" ref="${REF:-${GITHUB_REF:-}}" ref_name="${REF_NAME:-${GITHUB_REF_NAME:-dev}}" short_sha="$(git rev-parse --short=12 HEAD)" if [ "$event_name" = "push" ] && [ "$ref" = "refs/tags/${ref_name}" ]; then VERSION="${ref_name}-${short_sha}" RELEASE_TAG="${ref_name}" elif [ "$event_name" = "push" ] && [ "$ref" != "refs/heads/main" ]; then VERSION="${ref_name}-${short_sha}" RELEASE_TAG="${ref_name}" else BUILD_STAMP="$(date -u +%Y%m%d-%H%M%S)" VERSION="build-${BUILD_STAMP}-${short_sha}" RELEASE_TAG="$VERSION" fi RELEASE_NAME="$RELEASE_TAG" STAGE="meatshell-${VERSION}-windows-x86_64-gnu" mkdir -p "$STAGE" cp "${BUILD_DIR}/${TARGET}/release/${BIN}" "$STAGE/" cp README.md README.en.md CHANGELOG.md "$STAGE/" 2>/dev/null || true if command -v 7z >/dev/null 2>&1; then 7z a "${STAGE}.zip" "$STAGE" >/dev/null else zip -r "${STAGE}.zip" "$STAGE" fi echo "ASSET=${STAGE}.zip" >> "$GITHUB_ENV" echo "RELEASE_TAG=${RELEASE_TAG}" >> "$GITHUB_ENV" echo "RELEASE_NAME=${RELEASE_NAME}" >> "$GITHUB_ENV" - name: Select release token shell: bash env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} WORKFLOW_TOKEN: ${{ gitea.token }} run: | set -eu token="${RELEASE_TOKEN:-${GITEA_TOKEN:-${WORKFLOW_TOKEN:-}}}" if [ -z "$token" ]; then echo "No release token available; set RELEASE_TOKEN or GITEA_TOKEN." >&2 exit 1 fi echo "::add-mask::$token" echo "RELEASE_API_KEY=$token" >> "$GITHUB_ENV" - name: Publish Gitea release shell: bash env: API_TOKEN: ${{ env.RELEASE_API_KEY }} RELEASE_TAG_ENV: ${{ env.RELEASE_TAG }} RELEASE_NAME_ENV: ${{ env.RELEASE_NAME }} EVENT_NAME: ${{ gitea.event_name }} REF: ${{ gitea.ref }} REF_NAME: ${{ gitea.ref_name }} RUN_ID: ${{ gitea.run_id }} RUN_NUMBER: ${{ gitea.run_number }} GITEA_SERVER_URL_CTX: ${{ gitea.server_url }} GITEA_REPOSITORY_CTX: ${{ gitea.repository }} GITHUB_SERVER_URL_CTX: ${{ github.server_url }} GITHUB_REPOSITORY_CTX: ${{ github.repository }} run: | set -euo pipefail asset="${ASSET:?ASSET is not set}" event_name="${EVENT_NAME:-${GITHUB_EVENT_NAME:-}}" ref="${REF:-${GITHUB_REF:-}}" if [ -n "${RELEASE_TAG_ENV:-}" ]; then tag="$RELEASE_TAG_ENV" release_name="${RELEASE_NAME_ENV:-$tag}" release_body="Automated Windows GNU build for ${tag}." target_commitish="$(git rev-parse HEAD)" elif [ "$event_name" = "push" ] && [ "$ref" = "refs/heads/main" ]; then short_sha="$(git rev-parse --short=12 HEAD)" stamp="$(date -u +%Y%m%d-%H%M%S)" tag="build-${stamp}-${short_sha}" release_name="$tag" release_body="Automated Windows GNU build for main commit ${short_sha}." target_commitish="$(git rev-parse HEAD)" elif [ "$event_name" = "workflow_dispatch" ]; then short_sha="$(git rev-parse --short=12 HEAD)" run_id="${RUN_ID:-${GITHUB_RUN_ID:-${RUN_NUMBER:-${GITHUB_RUN_NUMBER:-$(date +%Y%m%d%H%M%S)}}}}" tag="manual-${run_id}" release_name="$tag" release_body="Manual Windows GNU build for commit ${short_sha}." target_commitish="$(git rev-parse HEAD)" else tag="${REF_NAME:-${GITHUB_REF_NAME:-}}" release_name="$tag" release_body="Automated Windows GNU build for ${tag}." target_commitish="$(git rev-parse HEAD)" fi token="${API_TOKEN:-${RELEASE_API_KEY:-}}" server="${GITEA_SERVER_URL_CTX:-${GITHUB_SERVER_URL_CTX:-${GITHUB_SERVER_URL:-}}}" repo="${GITEA_REPOSITORY_CTX:-${GITHUB_REPOSITORY_CTX:-${GITHUB_REPOSITORY:-}}}" if [ -z "$token" ]; then echo "RELEASE_API_KEY is not set." >&2 exit 1 fi if [ -z "$tag" ]; then echo "Tag name is empty." >&2 exit 1 fi if [ ! -f "$asset" ]; then echo "Release asset not found: $asset" >&2 exit 1 fi if [ -z "$server" ] || [ -z "$repo" ]; then read -r server repo < <(python3 - <<'PY' import re import subprocess from urllib.parse import urlparse remote = subprocess.check_output( ["git", "remote", "get-url", "origin"], text=True ).strip() if remote.startswith(("http://", "https://")): parsed = urlparse(remote) path = parsed.path.strip("/") if path.endswith(".git"): path = path[:-4] print(f"{parsed.scheme}://{parsed.netloc} {path}") else: match = re.match(r"[^@]+@([^:]+):(.+)$", remote) if not match: raise SystemExit(f"cannot parse git remote: {remote}") path = match.group(2) if path.endswith(".git"): path = path[:-4] print(f"https://{match.group(1)} {path}") PY ) fi server="${server%/}" api="$server/api/v1/repos/$repo" auth_header="Authorization: token $token" curl_api() { attempt=1 while true; do if curl --proxy "${HTTP_PROXY:-}" \ --insecure \ --fail --silent --show-error --location \ --connect-timeout 20 --max-time 180 \ -H "$auth_header" "$@"; then return 0 fi if [ "$attempt" -ge 8 ]; then return 1 fi sleep $((attempt * 3)) attempt=$((attempt + 1)) done } curl_status() { out="$1" shift attempt=1 while true; do status="$( curl --proxy "${HTTP_PROXY:-}" \ --insecure \ --silent --show-error --location \ --connect-timeout 20 --max-time 180 \ -H "$auth_header" \ -o "$out" -w "%{http_code}" \ "$@" || true )" if [ -n "$status" ] && [ "$status" != "000" ]; then printf '%s' "$status" return 0 fi if [ "$attempt" -ge 8 ]; then printf '%s' "${status:-000}" return 1 fi sleep $((attempt * 3)) attempt=$((attempt + 1)) done } echo "Checking Gitea API: $server/api/v1/version" curl_api "$server/api/v1/version" >/dev/null release_json="$(mktemp)" status="$(curl_status "$release_json" "$api/releases/tags/$tag" || true)" if [ "$status" = "404" ]; then payload="$( TAG="$tag" RELEASE_NAME="$release_name" RELEASE_BODY="$release_body" TARGET_COMMITISH="$target_commitish" python3 - <<'PY' import json import os tag = os.environ["TAG"] print(json.dumps({ "tag_name": tag, "target_commitish": os.environ["TARGET_COMMITISH"], "name": os.environ["RELEASE_NAME"], "body": os.environ["RELEASE_BODY"], "draft": False, "prerelease": False, })) PY )" status="$( curl_status "$release_json" \ -H "Content-Type: application/json" \ -X POST -d "$payload" \ "$api/releases" || true )" fi if [ "$status" != "200" ] && [ "$status" != "201" ]; then echo "Release lookup/create failed with HTTP $status" >&2 cat "$release_json" >&2 || true exit 1 fi release_id="$(python3 - "$release_json" <<'PY' import json import sys with open(sys.argv[1], encoding="utf-8") as fh: print(json.load(fh)["id"]) PY )" update_payload="$( RELEASE_NAME="$release_name" RELEASE_BODY="$release_body" python3 - <<'PY' import json import os print(json.dumps({ "name": os.environ["RELEASE_NAME"], "body": os.environ["RELEASE_BODY"], "draft": False, "prerelease": False, })) PY )" curl_api -H "Content-Type: application/json" \ -X PATCH -d "$update_payload" \ "$api/releases/$release_id" >/dev/null asset_name="$(basename "$asset")" asset_id="$( ASSET_NAME="$asset_name" python3 - "$release_json" <<'PY' import json import os import sys with open(sys.argv[1], encoding="utf-8") as fh: release = json.load(fh) assets = release.get("assets") or release.get("attachments") or [] for item in assets: if item.get("name") == os.environ["ASSET_NAME"]: print(item["id"]) break PY )" if [ -n "$asset_id" ]; then echo "Deleting existing release asset: $asset_name" curl_api -X DELETE "$api/releases/$release_id/assets/$asset_id" >/dev/null fi asset_query="$( ASSET_NAME="$asset_name" python3 - <<'PY' import os import urllib.parse print(urllib.parse.quote(os.environ["ASSET_NAME"])) PY )" echo "Uploading release asset: $asset_name" curl_api -X POST \ -F "attachment=@${asset}" \ "$api/releases/$release_id/assets?name=$asset_query" >/dev/null echo "✅ Release published successfully through proxy"