V3 作者资料页与工作环境卡片内容过长时自动独占一行 #2235
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: CI | |
| on: | |
| push: | |
| branches: | |
| - master | |
| - classic | |
| tags: | |
| - v*.*.* | |
| pull_request: | |
| branches: | |
| - master | |
| - classic | |
| paths-ignore: | |
| - '**.md' | |
| - '**.txt' | |
| - 'renovate.json' | |
| - '.editorconfig' | |
| - '.gitignore' | |
| - '.github/**' | |
| - '.idea/**' | |
| - '!.github/workflows/**' | |
| workflow_dispatch: | |
| env: | |
| ORIGINAL_PROJECT: "CeuiLiSA/Pixiv-Shaft" | |
| GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g" | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| name: Build apk | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Clone repo | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: 'true' | |
| fetch-depth: 0 | |
| - name: Set up JDK 21 | |
| uses: actions/setup-java@v4.2.2 | |
| with: | |
| java-version: '21' | |
| distribution: 'adopt' | |
| - name: Set up Gradle | |
| uses: gradle/actions/setup-gradle@v3 | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x gradlew | |
| # 签名材料从 Secrets 注入(keystore 与密码已从仓库移除)。 | |
| # keystore 解码到 $RUNNER_TEMP(仓库外)——历史 tag 的提交里还跟踪着 | |
| # keystore.jks,写进仓库根目录会让后面的 git checkout 撞 untracked 冲突。 | |
| # keystore.properties 从未被任何提交跟踪,放根目录安全。 | |
| # fork 的 PR 拿不到 Secrets → 跳过,build.gradle 降级为 debug 默认签名。 | |
| - name: Set up signing material | |
| env: | |
| KEYSTORE_B64: ${{ secrets.SHAFT_KEYSTORE_B64 }} | |
| KEYSTORE_PASSWORD: ${{ secrets.SHAFT_KEYSTORE_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.SHAFT_KEY_ALIAS }} | |
| KEY_PASSWORD: ${{ secrets.SHAFT_KEY_PASSWORD }} | |
| run: | | |
| if [ -n "$KEYSTORE_B64" ]; then | |
| echo "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/keystore.jks" | |
| { | |
| echo "storeFile=$RUNNER_TEMP/keystore.jks" | |
| echo "storePassword=$KEYSTORE_PASSWORD" | |
| echo "keyAlias=$KEY_ALIAS" | |
| echo "keyPassword=$KEY_PASSWORD" | |
| } > keystore.properties | |
| echo "Signing material ready" | |
| else | |
| echo "No signing secrets (fork build), falling back to default debug signing" | |
| fi | |
| - name: Detect branch | |
| if: github.ref_type == 'branch' | |
| run: | | |
| BRANCH_NAME=${GITHUB_REF#refs/heads/} | |
| echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV | |
| if [ "$BRANCH_NAME" = "master" ]; then | |
| echo "APK_SUFFIX=master" >> $GITHUB_ENV | |
| elif [ "$BRANCH_NAME" = "classic" ]; then | |
| echo "APK_SUFFIX=classic" >> $GITHUB_ENV | |
| fi | |
| # 官方仓库的包必须带正确的 HMAC 密钥,否则广场/聊天会被 shaft-api-v2 401, | |
| # 而 build.gradle 对空密钥是静默降级的(fork 友好),CI 不会自己红。 | |
| # 这里只比对 sha256 指纹:密钥是 256 bit 随机值,公开指纹无法反推。 | |
| # 指纹来源:服务器 /etc/shaft-api-v2/events-hmac-secret,`tr -d '\n' | sha256sum`。 | |
| # 轮换密钥时同步更新这里和 GitHub Secret SHAFT_EVENTS_HMAC。 | |
| # fork 发来的 PR 同样拿不到 Secrets,跳过。 | |
| - name: Verify SHAFT_EVENTS_HMAC secret | |
| if: github.repository == env.ORIGINAL_PROJECT && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) | |
| env: | |
| SHAFT_EVENTS_HMAC: ${{ secrets.SHAFT_EVENTS_HMAC }} | |
| EXPECTED_FINGERPRINT: e0f22bd504573b61863577d1369fc6c4c2e6dceab92cd5baddc3b9fe7fcfde6e | |
| run: | | |
| if [ -z "$SHAFT_EVENTS_HMAC" ]; then | |
| echo "::error::SHAFT_EVENTS_HMAC secret is empty; the APK would sign with an empty key" | |
| exit 1 | |
| fi | |
| ACTUAL=$(printf '%s' "$SHAFT_EVENTS_HMAC" | sha256sum | awk '{print $1}') | |
| if [ "$ACTUAL" != "$EXPECTED_FINGERPRINT" ]; then | |
| echo "::error::SHAFT_EVENTS_HMAC fingerprint mismatch: got $ACTUAL" | |
| exit 1 | |
| fi | |
| echo "SHAFT_EVENTS_HMAC fingerprint OK" | |
| # 单测跑在打包之前,红了就不出产物。只跑 debug 变体:release 变体的 | |
| # 测试代码相同,多跑一遍只是白费 R8 前的编译时间。 | |
| - name: Run unit tests | |
| env: | |
| SHAFT_EVENTS_HMAC: ${{ secrets.SHAFT_EVENTS_HMAC }} | |
| run: >- | |
| ./gradlew | |
| :app:testGithubDebugUnitTest | |
| :actionqueue:testDebugUnitTest | |
| :feeds:testDebugUnitTest | |
| :websocket:testDebugUnitTest | |
| :banner:testDebugUnitTest | |
| --continue | |
| # Room 会在编译时自动写出当前 schema;如果忘记提交,迁移测试会消费刚生成的 | |
| # 文件并照样通过,最终却丢失可供未来迁移使用的版本历史。 | |
| - name: Verify Room schemas are committed | |
| run: | | |
| changes="$(git status --porcelain --untracked-files=all -- app/schemas)" | |
| if [ -n "$changes" ]; then | |
| echo "::error::Room schemas changed during compilation; regenerate and commit app/schemas" | |
| printf '%s\n' "$changes" | |
| exit 1 | |
| fi | |
| # app lint 只分析宿主自身;各 library 必须显式列出,才能阻止模块内新问题绕过门禁。 | |
| - name: Run Android lint | |
| run: >- | |
| ./gradlew | |
| :app:lintGithubDebug | |
| :actionqueue:lintDebug | |
| :feeds:lintDebug | |
| :websocket:lintDebug | |
| :banner:lintDebug | |
| :models:lintDebug | |
| :flowlayout-lib:lintDebug | |
| :progressmanager:lintDebug | |
| :witstudio:lintDebug | |
| --continue | |
| # progressmanager 历史配置了 abortOnError=false;统一检查 XML,避免该模块的 | |
| # lint Error 被 Gradle 任务吞掉后让 CI 误报成功。 | |
| - name: Verify lint reports are error-free | |
| run: | | |
| failed=0 | |
| reports=( | |
| app/build/reports/lint-results-githubDebug.xml | |
| actionqueue/build/reports/lint-results-debug.xml | |
| feeds/build/reports/lint-results-debug.xml | |
| websocket/build/reports/lint-results-debug.xml | |
| banner/build/reports/lint-results-debug.xml | |
| models/build/reports/lint-results-debug.xml | |
| flowlayout-lib/build/reports/lint-results-debug.xml | |
| progressmanager/build/reports/lint-results-debug.xml | |
| witstudio/build/reports/lint-results-debug.xml | |
| ) | |
| for report in "${reports[@]}"; do | |
| if [ ! -f "$report" ]; then | |
| echo "::error file=$report::Expected lint XML report was not generated" | |
| failed=1 | |
| elif grep -Eq 'severity="(Error|Fatal)"' "$report"; then | |
| echo "::error file=$report::Android lint reported an error" | |
| failed=1 | |
| fi | |
| done | |
| exit "$failed" | |
| - name: Upload lint reports | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: lint-reports-${{ github.run_id }} | |
| path: '**/build/reports/lint-results*' | |
| if-no-files-found: ignore | |
| - name: Upload unit test reports | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: unit-test-reports-${{ github.run_id }} | |
| path: | | |
| **/build/reports/tests/ | |
| **/build/test-results/ | |
| if-no-files-found: ignore | |
| - name: Build debug apk | |
| if: github.ref_type == 'branch' | |
| env: | |
| SHAFT_EVENTS_HMAC: ${{ secrets.SHAFT_EVENTS_HMAC }} | |
| run: ./gradlew assembleGithubDebug | |
| - name: Upload debug artifact | |
| if: github.ref_type == 'branch' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: apk-debug-${{ env.APK_SUFFIX }} | |
| path: app/build/outputs/apk/github/debug | |
| - name: Get tag name | |
| if: startsWith(github.ref, 'refs/tags/') && github.repository == env.ORIGINAL_PROJECT | |
| run: | | |
| set -x | |
| version_tag=${GITHUB_REF/refs\/tags\//} | |
| version_tag_short=`echo $version_tag | awk 'NR==1,/v/{sub(/v/, "");print}'` | |
| echo "VERSION_TAG=$version_tag" >> $GITHUB_ENV | |
| echo "VERSION_TAG_SHORT=$version_tag_short" >> $GITHUB_ENV | |
| - name: Extract tag message before checkout | |
| if: startsWith(github.ref, 'refs/tags/') && github.repository == env.ORIGINAL_PROJECT | |
| run: | | |
| TAG_MESSAGE=$(git cat-file tag "$VERSION_TAG" 2>/dev/null | sed '1,/^$/d' || echo "") | |
| if [ -z "$TAG_MESSAGE" ]; then | |
| TAG_MESSAGE="Release $VERSION_TAG" | |
| fi | |
| # Persist to file since env vars lose multiline | |
| echo "$TAG_MESSAGE" > /tmp/tag_message.txt | |
| - name: Detect and checkout correct branch | |
| if: startsWith(github.ref, 'refs/tags/') && github.repository == env.ORIGINAL_PROJECT | |
| run: | | |
| # Auto-detect which branch this tagged commit belongs to | |
| TAG_COMMIT=$(git rev-parse HEAD) | |
| CLASSIC_CONTAINS=$(git merge-base --is-ancestor "$TAG_COMMIT" origin/classic && echo yes || echo no) | |
| MASTER_CONTAINS=$(git merge-base --is-ancestor "$TAG_COMMIT" origin/master && echo yes || echo no) | |
| if [ "$CLASSIC_CONTAINS" = "yes" ] && [ "$MASTER_CONTAINS" = "no" ]; then | |
| TAG_BRANCH=classic | |
| elif [ "$MASTER_CONTAINS" = "yes" ] && [ "$CLASSIC_CONTAINS" = "no" ]; then | |
| TAG_BRANCH=master | |
| elif [ "$CLASSIC_CONTAINS" = "yes" ] && [ "$MASTER_CONTAINS" = "yes" ]; then | |
| # Commit exists on both branches, pick the one whose tip is closer | |
| CLASSIC_DIST=$(git rev-list --count "$TAG_COMMIT"..origin/classic) | |
| MASTER_DIST=$(git rev-list --count "$TAG_COMMIT"..origin/master) | |
| if [ "$CLASSIC_DIST" -le "$MASTER_DIST" ]; then | |
| TAG_BRANCH=classic | |
| else | |
| TAG_BRANCH=master | |
| fi | |
| else | |
| TAG_BRANCH=master | |
| fi | |
| echo "TAG_BRANCH=$TAG_BRANCH" >> $GITHUB_ENV | |
| echo "Detected branch: $TAG_BRANCH" | |
| git checkout "origin/$TAG_BRANCH" | |
| git submodule update --init --recursive | |
| - name: Build release apk | |
| if: startsWith(github.ref, 'refs/tags/') && github.repository == env.ORIGINAL_PROJECT | |
| env: | |
| SHAFT_EVENTS_HMAC: ${{ secrets.SHAFT_EVENTS_HMAC }} | |
| run: ./gradlew assembleGithubRelease --stacktrace | |
| - name: Deal with release apk | |
| if: startsWith(github.ref, 'refs/tags/') && github.repository == env.ORIGINAL_PROJECT | |
| run: | | |
| cp app/build/outputs/apk/github/release/app-github-release.apk app-release.apk | |
| if [ "$TAG_BRANCH" = "classic" ]; then | |
| APK_NAME="PixShaft_${VERSION_TAG_SHORT}_classic.apk" | |
| else | |
| APK_NAME="PixShaft_${VERSION_TAG_SHORT}.apk" | |
| fi | |
| cp app-release.apk "$APK_NAME" | |
| echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV | |
| sha1=`sha1sum app-release.apk | awk '{ print toupper($1) }'` | |
| sha256=`sha256sum app-release.apk | awk '{ print toupper($1) }'` | |
| echo "APK_SHA1=$sha1" >> $GITHUB_ENV | |
| echo "APK_SHA256=$sha256" >> $GITHUB_ENV | |
| - name: Prepare release notes | |
| if: startsWith(github.ref, 'refs/tags/') && github.repository == env.ORIGINAL_PROJECT | |
| run: | | |
| TAG_MESSAGE=$(cat /tmp/tag_message.txt) | |
| # Write release notes with SHA checksums | |
| { | |
| echo "$TAG_MESSAGE" | |
| echo "" | |
| echo '```' | |
| echo "SHA-1: $APK_SHA1" | |
| echo "SHA-256: $APK_SHA256" | |
| echo '```' | |
| } > release_notes.md | |
| - name: Draft release | |
| if: startsWith(github.ref, 'refs/tags/') && github.repository == env.ORIGINAL_PROJECT | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: ${{ env.VERSION_TAG }} | |
| name: ${{ env.VERSION_TAG }} | |
| body_path: release_notes.md | |
| files: | | |
| ${{ env.APK_NAME }} | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| instrumentation: | |
| name: Critical androidTest (API 35) | |
| runs-on: ubuntu-24.04 | |
| needs: build | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Clone repo | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: 'true' | |
| - name: Set up JDK 21 | |
| uses: actions/setup-java@v4.2.2 | |
| with: | |
| java-version: '21' | |
| distribution: 'adopt' | |
| - name: Set up Gradle | |
| uses: gradle/actions/setup-gradle@v3 | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x gradlew | |
| - name: Enable KVM | |
| run: | | |
| echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules | |
| sudo udevadm control --reload-rules | |
| sudo udevadm trigger --name-match=kvm | |
| # MigrationTestHelper 必须对未混淆的目标 APK 跑,避免测试夹具跨 APK 调用被 R8 | |
| # 重命名的 Kotlin stdlib;真正依赖混淆结果的契约测试则单独对 Release 跑。 | |
| # x86_64 只对本次测试构建生效,正式 APK 仍保持 arm64-v8a。 | |
| - name: Run critical instrumented tests | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| env: | |
| # 一次性测试键,仅用于证明 native signer 能加载;不是生产凭证。 | |
| SHAFT_EVENTS_HMAC: ci-instrumentation-only-not-a-secret | |
| with: | |
| api-level: 35 | |
| target: google_apis | |
| arch: x86_64 | |
| profile: pixel_7_pro | |
| script: | | |
| ./gradlew :app:connectedGithubDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=ceui.lisa.database.AppDatabaseMigrationTest -PSHAFT_TEST_ABI=x86_64 --stacktrace | |
| ./gradlew :app:connectedGithubReleaseAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=ceui.lisa.ExampleInstrumentedTest,ceui.lisa.model.AppApiGsonR8InstrumentedTest,ceui.pixiv.shaftapi.ShaftHmacInstrumentedTest,ceui.pixiv.ui.search.SearchRiskPolicyInstrumentedTest -PSHAFT_TEST_BUILD_TYPE=release -PSHAFT_TEST_ABI=x86_64 --stacktrace | |
| ./gradlew :actionqueue:connectedDebugAndroidTest --stacktrace | |
| - name: Upload instrumented test reports | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-test-reports-${{ github.run_id }} | |
| path: | | |
| **/build/reports/androidTests/ | |
| **/build/outputs/androidTest-results/ | |
| if-no-files-found: ignore |