fix(agent): 还原流式输出贴底跟随为 1.11.1 前实现 #77
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |