Skip to content

Merge pull request #1077 from wangwang-code/patch-8-31-1 #2241

Merge pull request #1077 from wangwang-code/patch-8-31-1

Merge pull request #1077 from wangwang-code/patch-8-31-1 #2241

Workflow file for this run

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