Skip to content

docs(readme): retake screenshots at full resolution #26

docs(readme): retake screenshots at full resolution

docs(readme): retake screenshots at full resolution #26

Workflow file for this run

# 打 tag 发版。构建只发生在这里 —— 本机不产出、也拿不到发布密钥,
# 所以「这个 APK 是从这份源码编出来的」这句话有据可查。
#
# 两份构建来源证明:job `release` 里的 attest-build-provenance(GitHub 自己核,快),
# 以及 job `provenance` 走 slsa-github-generator 出的 SLSA Build L3 证明。后者跑在
# 隔离的可复用 workflow 里,拿不到这个仓库的构建脚本和签名密钥 —— 这正是 L2 与 L3
# 的区别所在。
#
# 触发:推一个 `v` 开头的 tag,例如 `git tag v0.1.0 && git push origin v0.1.0`。
# 版本号取自 tag(去掉 `v`),传给 Gradle 的 -PbilbyVersion,不写在源码里。
#
# 需要两个仓库 Secret:
# BILBY_KEYSTORE_BASE64 —— 签名库的 base64
# BILBY_KEYSTORE_PASSWORD / BILBY_KEY_ALIAS / BILBY_KEY_PASSWORD
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
# 构建来源证明要签名,需要这两项。它是「CI 构建」这句话的凭据本身。
id-token: write
attestations: write
jobs:
release:
runs-on: ubuntu-latest
outputs:
# 交给下面那个隔离的 provenance job。传的是摘要不是文件:generator 不信任、
# 也不需要拿到产物本身。
hashes: ${{ steps.collect.outputs.hashes }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v5
with:
distribution: temurin
# compileOptions 是 21,这里不能更低。
java-version: "21"
# 停在 v5,不跟到 v6。v6 把缓存拆成了闭源的 gradle-actions-caching,升上去
# 等于接受 Gradle 的商业 Terms of Use —— 这个仓库是 GPL-3.0,不为了一个
# 构建缓存把一份商业条款引进发布链路。v5 与 v4 的差别只是 Node 24。
- uses: gradle/actions/setup-gradle@v5
- name: Derive version from tag
id: version
run: echo "name=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
# 密钥只在这一步落到磁盘,用完随 runner 一起销毁。
- name: Restore keystore
env:
KEYSTORE_BASE64: ${{ secrets.BILBY_KEYSTORE_BASE64 }}
run: echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/bilby.jks"
# 单测只有 debug 变体有:AGP 按 testBuildType(默认 debug)生成任务,
# 写成 testReleaseUnitTest 会在 CI 上报 "Task not found"。
- name: Test
run: ./gradlew testDebugUnitTest -PbilbyVersion=${{ steps.version.outputs.name }}
- name: Build
env:
BILBY_KEYSTORE: ${{ runner.temp }}/bilby.jks
BILBY_KEYSTORE_PASSWORD: ${{ secrets.BILBY_KEYSTORE_PASSWORD }}
BILBY_KEY_ALIAS: ${{ secrets.BILBY_KEY_ALIAS }}
BILBY_KEY_PASSWORD: ${{ secrets.BILBY_KEY_PASSWORD }}
run: ./gradlew assembleRelease -PbilbyVersion=${{ steps.version.outputs.name }}
# 装错架构会装不上,所以文件名必须一眼看出是哪个变体。
- name: Collect artifacts
id: collect
run: |
mkdir -p dist
version="${{ steps.version.outputs.name }}"
for apk in app/build/outputs/apk/release/*.apk; do
variant=$(basename "$apk" | sed -E 's/^app-(.*)-release\.apk$/\1/')
cp "$apk" "dist/bilby-$version-$variant.apk"
done
# R8 的 mapping 一起发:release 崩溃栈是混淆过的,没有它谁也读不了用户贴的日志。
cp app/build/outputs/mapping/release/mapping.txt "dist/bilby-$version-mapping.txt"
(cd dist && sha256sum * > SHA256SUMS.txt)
ls -l dist
# SLSA generator 要的是 base64 过的 `sha256sum` 输出。只列 APK:
# 证明覆盖的是"用户会装进手机的那几个文件"。
echo "hashes=$(cd dist && sha256sum *.apk | base64 -w0)" >> "$GITHUB_OUTPUT"
# 证明这些文件确实由这个 workflow、从这个 commit 构建。
# 校验:gh attestation verify bilby-<版本>-<变体>.apk --repo <owner>/<repo>
- name: Attest build provenance
uses: actions/attest-build-provenance@v4
with:
subject-path: dist/*.apk
- name: Publish release
uses: softprops/action-gh-release@v3
with:
draft: true
files: dist/*
body: |
## 安装
universal 为通用包;arm64-v8a、armeabi-v7a、x86_64 为对应架构的精简包,体积略小。四者为同一版本的不同打包,架构不匹配时无法安装。
## 校验
APK 由 GitHub Actions 从本 tag 构建并签名。
- 校验和:`SHA256SUMS.txt`
- 构建来源:`gh attestation verify <文件> --repo ${{ github.repository }}`
- SLSA Build L3 证明:`multiple.intoto.jsonl`,用 [slsa-verifier](https://github.com/slsa-framework/slsa-verifier) 校验
```
slsa-verifier verify-artifact <文件> --provenance-path multiple.intoto.jsonl --source-uri github.com/${{ github.repository }}
```
`mapping.txt` 用于还原崩溃堆栈。
# SLSA Build Level 3。
#
# 上面那个 `attest-build-provenance` 已经能证明"这些文件由这个 workflow 从这个 commit
# 构建",但它和构建跑在同一个 job 里 —— 按 SLSA 的说法,构建过程能影响到生成证明的过程,
# 所以只到 L2。这个 job 用 slsa-github-generator 的可复用 workflow:它在自己的
# 上下文里跑,拿不到本仓库的构建脚本,也拿不到签名密钥,证明因此不可伪造(L3)。
#
# 两份证明都留着,它们回答的不是同一个问题,验证方式也不同:
# gh attestation verify <文件> --repo <owner>/<repo> # 快,GitHub 自己核
# slsa-verifier verify-artifact <文件> # --provenance-path multiple.intoto.jsonl # --source-uri github.com/<owner>/<repo> # 按 SLSA 规范核
provenance:
needs: [release]
permissions:
# 证明要签名,签名要 OIDC 身份。
id-token: write
contents: write
# generator 要读本次 workflow run 的元数据来填 provenance。
actions: read
# 这个 job 的日志里有一片 "Node.js 20 is deprecated" —— **来自 generator 内部**
# (`detect-workflow-js`、`compute-sha256`、它自己 pin 的 checkout),不是本仓库能改的:
# v2.1.0 已经是上游最新版。runner 目前会把它们强制跑在 Node 24 上,只是告警。
# 本仓库自己引的 action 都已在 Node 24 一代。
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: ${{ needs.release.outputs.hashes }}
# **不让 generator 自己上传。** 它按 tag 查 release,而 GitHub 的按-tag 接口看不到
# 草稿;上面那个 job 发的正是草稿,于是它查不到、转而**新建并公开发布**一个同名
# release,里面只有一份 .intoto.jsonl,正文空白。v0.7.0 上真的发生过:发布页上
# 一个 tag 挂了两个 release,公开的那个还是空的。
#
# 改成让它把证明留作 workflow 产物,由下面那个 job 按 release id 挂到草稿上。
upload-assets: false
# 把 SLSA 证明挂到上面那个草稿 release 上。
#
# 按 **release id** 找,不按 tag:`gh release view <tag>` 和 REST 的 releases/tags/<tag>
# 都不返回草稿,只有列表接口看得到它。
attach-provenance:
needs: [release, provenance]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v8
with:
name: ${{ needs.provenance.outputs.provenance-name }}
- name: Attach provenance to the draft release
env:
GH_TOKEN: ${{ github.token }}
NAME: ${{ needs.provenance.outputs.provenance-name }}
run: |
id=$(gh api "repos/$GITHUB_REPOSITORY/releases" --paginate \
--jq ".[] | select(.tag_name == \"$GITHUB_REF_NAME\") | .id" | head -1)
if [ -z "$id" ]; then
echo "找不到 $GITHUB_REF_NAME 的 release" >&2
exit 1
fi
# 用 uploads 主机传二进制:gh api 走的是 api.github.com,那台不收附件。
curl -sS --fail-with-body -X POST \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$NAME" \
"https://uploads.github.com/repos/$GITHUB_REPOSITORY/releases/$id/assets?name=$NAME" \
> /dev/null