Skip to content

fix(agent): 首字超时放宽到 5 分钟,流式读超时改为无限制 #72

fix(agent): 首字超时放宽到 5 分钟,流式读超时改为无限制

fix(agent): 首字超时放宽到 5 分钟,流式读超时改为无限制 #72

Workflow file for this run

name: Android Release
# 打 Tag(如 v1.0.0)时触发:构建三个签名 Release APK
# - universal : arm64-v8a + x86_64,含 arm + x86 两套容器镜像(通用,体积最大)
# - armsolo : 仅 arm64-v8a,仅含 arm 镜像(真机首选,体积约为 universal 的一半)
# - x86solo : 仅 x86_64,仅含 x86 镜像(x86 模拟器/Chromebook,体积约为 universal 的一半)
# 创建 GitHub Release 并把三个 APK 作为资产发布。
on:
push:
tags:
- 'v*'
# 允许在 Actions 页面手动触发,便于验证构建链路。
workflow_dispatch:
jobs:
build:
name: Build & Release
runs-on: ubuntu-latest
permissions:
contents: write # 创建 Release 与上传资产需要写权限
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# versionCode 由 build.gradle.kts 的 gitCommitCount() 取 git rev-list --count HEAD,
# 浅克隆(默认 depth=1)会得到 1,必须拉完整历史,否则 Release APK 版本号错乱。
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
# 复用 Gradle 构建缓存,加速后续构建。
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
# 打印当前 Tag,Tag 驱动直接生成 versionName。
# 仅打 tag 触发时校验;workflow_dispatch(手动,无 tag)跳过。
- name: Display release tag info
if: startsWith(github.ref, 'refs/tags/v')
run: |
set -e
tag_ver="${GITHUB_REF_NAME#v}" # 1.7.0 / 1.7.0-rc1 / ...
echo "tag version (without v) = $tag_ver"
echo "✓ tag 将直接驱动应用 versionName ($tag_ver)"
# 校验 versionCode 单调递增:versionCode = git commit count,rebase/squash 改写历史
# 可能让 commit count 变小,导致新版本 versionCode < 旧版本、升级判定失效。
# 取按提交时间倒序最近的非当前 tag,算其 commit count,要求当前 HEAD 不小于上个 Release。
# 允许相等:rc 转正式版时常在同一提交上打 tag,versionCode 相等是正常的。
# 仅打 tag 触发时校验;workflow_dispatch(手动,无 tag)跳过。
- name: Verify versionCode monotonic
if: startsWith(github.ref, 'refs/tags/v')
run: |
set -e
cur_vc=$(git rev-list --count HEAD)
prev_tag=$(git tag --sort=-creatordate | grep -vE "^${GITHUB_REF_NAME}$" | head -1)
if [[ -z "$prev_tag" ]]; then
echo "无历史 tag,跳过 versionCode 单调校验(首次发版)"
echo "当前 versionCode(commit count) = $cur_vc"
exit 0
fi
prev_vc=$(git rev-list --count "$prev_tag")
echo "上个 Release($prev_tag) versionCode = $prev_vc"
echo "当前(${GITHUB_REF_NAME}) versionCode = $cur_vc"
if [[ "$cur_vc" -lt "$prev_vc" ]]; then
echo "::error::versionCode 回退: 当前 $cur_vc < 上个 Release($prev_tag) $prev_vc。通常因 rebase/squash 改写历史使 commit count 变小。请确保新版本在旧 tag 之上线性推进。"
exit 1
fi
echo "✓ versionCode 单调递增: $prev_vc -> $cur_vc"
- name: Restore release keystore
env:
# keystore 文件以 base64 存入 Secret,避免二进制文件被破坏。
# 还原到 app/aicode.jks,与 build.gradle.kts 期望的位置一致。
KEYSTORE_BASE64: ${{ secrets.AICODE_KEYSTORE_BASE64 }}
run: |
echo "$KEYSTORE_BASE64" | base64 -d > app/aicode.jks
echo "keystore restored ($(stat -c%s app/aicode.jks) bytes)"
- name: Generate keystore.properties
env:
KEYSTORE_PASSWORD: ${{ secrets.AICODE_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.AICODE_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.AICODE_KEY_PASSWORD }}
run: |
cat > app/keystore.properties <<EOF
storeFile=aicode.jks
storePassword=$KEYSTORE_PASSWORD
keyAlias=$KEY_ALIAS
keyPassword=$KEY_PASSWORD
EOF
echo "app/keystore.properties generated"
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
# 发版质量门禁:跑单元测试,失败即中止,避免发出回归包。
# 只跑 universal debug 变体:单测逻辑与 flavor 无关,跑全 flavor 6 变体是重复编译。
- name: Run unit tests
run: ./gradlew :app:testUniversalDebugUnitTest
# 一次性构建全部 flavor 的 release APK(universal/armsolo/x86solo)
# --stacktrace:构建失败时输出完整调用栈,便于在 CI 定位 OOM/内存类根因。
- name: Build all flavor Release APKs
run: ./gradlew assembleRelease --stacktrace
# 给三个 APK 重命名为「包名-架构-版本」格式,架构用真实 ABI 名便于用户识别:
# aicode-arm64-v1.0.0-rc1.apk (armsolo)
# aicode-x86_64-v1.0.0-rc1.apk (x86solo)
# aicode-universal-v1.0.0-rc1.apk (universal,含两套镜像)
- name: Rename APKs by flavor
run: |
mkdir -p dist
ver="${GITHUB_REF_NAME:-manual}" # tag 如 v1.0.0-rc1;手动触发时为 manual-N
declare -A abi=( [armsolo]=arm64 [x86solo]=x86_64 [universal]=universal )
for f in universal armsolo x86solo; do
src="app/build/outputs/apk/$f/release/app-$f-release.apk"
if [[ -f "$src" ]]; then
out="dist/aicode-${abi[$f]}-${ver}.apk"
cp "$src" "$out"
echo "packaged $out"
else
echo "::warning::missing $src"
fi
done
ls -lh dist/
- name: Determine release name
id: release
run: |
# 手动触发(无 tag)时用 run id 作为临时名,避免破坏 tag 发布。
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
else
echo "tag=manual-${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
fi
- name: Determine prerelease flag
id: prerelease
run: |
tag="${{ steps.release.outputs.tag }}"
if [[ "$tag" =~ -(rc|dev|beta|alpha)([.0-9]*)?$ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
echo "检测到预发布后缀,标记为 pre-release"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
echo "正式版,不标记 pre-release"
fi
# GitHub 的 generate_release_notes 只列 merged PR,直接 push 的 commit 不会出现。
# 本仓库在 main 上直接 commit,不走 PR 流程,所以手动用 git log 生成 changelog。
- name: Generate changelog from git log
id: changelog
if: startsWith(github.ref, 'refs/tags/v')
run: |
cur_tag="${GITHUB_REF_NAME}"
# 正式版(vX.Y.Z 无后缀)从上一个正式版起算,包含整个发布周期(含全部 RC)的提交,
# 保证应用内检测更新能看到完整日志;RC/预发布从最近创建的 tag 起算(增量)。
if [[ "$cur_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
prev_tag=$(git tag --sort=-creatordate | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$" | grep -vE "^${cur_tag}$" | head -1)
echo "正式版:从上一个正式版 ${prev_tag:-无} 起算 changelog(含全部 RC 提交)"
else
prev_tag=$(git tag --sort=-creatordate | grep -vE "^${cur_tag}$" | head -1)
echo "预发布:从最近 tag ${prev_tag:-无} 起算 changelog"
fi
if [[ -z "$prev_tag" ]]; then
echo "无历史 tag,列出最近 30 条提交"
log=$(git log --oneline --no-decorate -30)
else
echo "生成 ${prev_tag}..${cur_tag} 的 changelog"
log=$(git log --oneline --no-decorate "${prev_tag}..HEAD")
fi
# 转义为 GitHub Actions 输出(多行用 EOF 分隔符)
delimiter="EOF_$(date +%s)"
echo "body<<${delimiter}" >> "$GITHUB_OUTPUT"
echo "## What's Changed" >> "$GITHUB_OUTPUT"
echo "" >> "$GITHUB_OUTPUT"
if [[ -z "$log" ]]; then
echo "- 无新增提交(与上次发布同一提交)" >> "$GITHUB_OUTPUT"
else
echo "$log" | while IFS= read -r line; do
# 提取 commit hash 和 message,格式化为列表项
sha=$(echo "$line" | cut -d' ' -f1)
msg=$(echo "$line" | cut -d' ' -f2-)
echo "- ${msg} (${sha})" >> "$GITHUB_OUTPUT"
done
fi
echo "" >> "$GITHUB_OUTPUT"
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${prev_tag}...${cur_tag}" >> "$GITHUB_OUTPUT"
echo "${delimiter}" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release & Upload assets
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.release.outputs.tag }}
name: Release ${{ steps.release.outputs.tag }}
body: ${{ steps.changelog.outputs.body }}
prerelease: ${{ steps.prerelease.outputs.is_prerelease }}
# 产物名约定 aicode-<abi>-<版本>.apk(见 Rename 步骤),故 glob 把 ABI 段放 aicode- 紧后、版本段用 * 兜底。
# 不要写成 aicode-*-<abi>.apk——那会把 abi 误放末段,与 aicode-<abi>-<ver> 永远匹配不上(rc3 翻车原因)。
files: |
dist/aicode-universal-*.apk
dist/aicode-arm64-*.apk
dist/aicode-x86_64-*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 同步 Release 附件到 GitCode(面向访问不了 GitHub 的用户):
# GitCode Release 走官方 API v5:POST 创建(tag 不存在时自动在 main 上创建,
# 无需像 Gitee 那样等官方镜像同步 tag)、GET upload_url 拿预签名 OBS 地址后
# PUT 二进制上传(响应 headers 参与签名,必须全带)。
# 前置配置:
# 1. GitCode 个人访问令牌 → GitHub Secret: GITCODE_TOKEN
# 2. (可选)GitCode 用户名/仓库名 → GitHub Variables: GITCODE_OWNER / GITCODE_REPO,不配则默认与 GitHub 同名
- name: Sync release assets to GitCode
if: startsWith(github.ref, 'refs/tags/v')
env:
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
GITCODE_OWNER: ${{ vars.GITCODE_OWNER }}
GITCODE_REPO: ${{ vars.GITCODE_REPO }}
GITHUB_REPO: ${{ github.repository }}
TAG: ${{ steps.release.outputs.tag }}
RELEASE_NAME: Release ${{ steps.release.outputs.tag }}
RELEASE_BODY: ${{ steps.changelog.outputs.body }}
run: |
set -euo pipefail
if [[ -z "$GITCODE_TOKEN" ]]; then
echo "::warning::未配置 GITCODE_TOKEN,跳过 GitCode 同步。请到 Settings→Secrets and variables→Actions 添加(GitCode 个人访问令牌)。"
exit 0
fi
owner="${GITCODE_OWNER:-${GITHUB_REPO%/*}}"
repo="${GITCODE_REPO:-${GITHUB_REPO#*/}}"
api="https://api.gitcode.com/api/v5/repos/${owner}/${repo}"
echo "目标 GitCode 仓库: ${owner}/${repo}"
# 先验证 token 与仓库可达,避免把鉴权错误误判成其它问题
if ! curl -fsS -H "Authorization: Bearer ${GITCODE_TOKEN}" "${api}" > /dev/null; then
echo "::error::无法访问 GitCode 仓库 ${owner}/${repo},请检查 GITCODE_TOKEN 与 GITCODE_OWNER/GITCODE_REPO"
exit 1
fi
# 创建或复用 Release:GitCode 在 tag 缺失时会自动于 target_commitish(main) 创建,
# 无需等待镜像同步;同 tag 已存在返回 409 视为复用(幂等)。
payload=$(python3 -c 'import json,os; print(json.dumps({"tag_name": os.environ["TAG"], "name": os.environ["RELEASE_NAME"], "body": os.environ["RELEASE_BODY"], "target_commitish": "main"}, ensure_ascii=False))')
code=$(curl -sS -o /tmp/gc_release.json -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${GITCODE_TOKEN}" -H "Content-Type: application/json" \
-d "$payload" "${api}/releases")
if [[ "$code" == "200" || "$code" == "201" ]]; then
echo "✓ Release ${TAG} 已创建"
elif [[ "$code" == "409" ]]; then
echo "Release ${TAG} 已存在,复用"
else
echo "::error::创建 GitCode Release 失败(HTTP $code):$(head -c 300 /tmp/gc_release.json)"
exit 1
fi
# 上传 APK:GET upload_url 取预签名地址,再 PUT 二进制(headers 全带,参与 OBS 签名校验)。
# 加超时与重试:GitCode 传输偶发挂起,无超时会无限卡住整个 job。
for apk in dist/aicode-*.apk; do
[[ -f "$apk" ]] || continue
fname=$(basename "$apk")
echo "上传 ${fname}($(numfmt --to=iec "$(stat -c%s "$apk")"))..."
uploaded=false
for attempt in 1 2 3; do
if curl -fsS --max-time 120 -H "Authorization: Bearer ${GITCODE_TOKEN}" \
"${api}/releases/${TAG}/upload_url?file_name=${fname}" -o /tmp/gc_up.json && \
python3 scripts/gitcode-upload.py "$apk" /tmp/gc_up.json
then
echo " ✓ 已上传"
uploaded=true
break
fi
echo " ⚠ 上传失败(第 ${attempt} 次),10s 后重试..."
sleep 10
done
if [[ "$uploaded" != true ]]; then
echo "::warning::${fname} 连续 3 次上传失败,跳过(GitHub Release 不受影响)"
fi
done
echo "完成: https://gitcode.com/${owner}/${repo}/releases"
# 历史 Release 幂等补齐(默认保留最新 3 个正式版,不包含 RC;已同步的跳过)。
# GitCode 无公开附件配额限制,且删除 Release 只能删 tag(连带删除,副作用大),
# 故不做自动清理,脚本只负责增量补齐缺失附件。
- name: Backfill historical releases to GitCode
if: startsWith(github.ref, 'refs/tags/v')
env:
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
run: python3 scripts/sync-gitcode-releases.py