501 lines
18 KiB
YAML
501 lines
18 KiB
YAML
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"
|