Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@

收到 review 之後請避免 force push,否則審查者看不到你改了什麼。

送出之前,請在本機跑過 AGENTS.md「Before pushing」那份清單 —— CI 跑的就是
同一份,在本機失敗比在 CI 失敗快得多。
送出之前跑 `tool/commit.sh --push` —— CI 跑的就是同一份,在本機失敗比在 CI
失敗快得多,而且 `.githooks/pre-push` 本來就會替你跑一次。

**這份描述不會被任何 gate 檢查,也不會進 main。** 被檢查的是分支上每一則
commit 訊息,進 main 的也是它們 —— 這個 repo 只開放 rebase 合併。所以要寫給
未來的人看的東西,寫在 commit 訊息裡,不是這裡。
-->

## 這個 PR 做了什麼
Expand Down
18 changes: 10 additions & 8 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ jobs:
java-version: "25"
cache: "gradle"

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
# Flutter and Dart come from mise.toml, the same pin CI and every laptop
# use. Not subosito/flutter-action with `channel: stable`: that resolves
# to whatever stable is on the day, so this job could ship an artifact
# built against an SDK nobody chose, and nothing in the build log would
# say so. See AGENTS.md → Toolchain.
- name: Install toolchain (mise)
uses: jdx/mise-action@v4

- name: Cache Gradle
uses: actions/cache@v4
Expand Down Expand Up @@ -85,8 +87,8 @@ jobs:

- name: Install dependencies and run build_runner
run: |
flutter pub get
dart run build_runner build --delete-conflicting-outputs
bash tool/dev/deps.sh
bash tool/dev/codegen.sh

- name: Decode keystore
run: |
Expand All @@ -102,7 +104,7 @@ jobs:
EOF
Comment on lines 93 to 104

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
秘密應透過 env: 區塊傳遞給需要的步驟,而不是直接在 run: 中使用 ${{ secrets.X }}。這可以避免秘密意外洩露到日誌中,並符合安全最佳實踐。

Suggestion:

Suggested change
- name: Decode keystore
run: |
echo "$KEYSTORE_BASE64" | base64 --decode > android/app/my-release-key.jks
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
- name: Create key.properties
run: |
cat > android/key.properties << EOF
storePassword=$KEYSTORE_PASSWORD
keyPassword=$KEY_PASSWORD
keyAlias=$KEY_ALIAS
storeFile=my-release-key.jks
EOF
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}


- name: Build Release APK
run: flutter build apk --release
run: bash tool/dev/build.sh android

- name: Upload Artifacts
uses: actions/upload-artifact@v4
Expand Down
27 changes: 0 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,33 +89,6 @@ jobs:

echo "branch gate: rebased on $BASE_REF, no merge commits"

# The commits on the branch are not what lands on main. This repo
# squash-merges, so GitHub builds the merged commit out of the PR's title
# and description — which this gate never saw. That is not hypothetical:
# `332fb8f3 Fix report (#529)` is on main, passes nothing, and can never
# be repaired. Check the message that will actually be committed.
- name: Commit message gate (squashed PR title)
if: github.event_name == 'pull_request'
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# Via the environment, never through expression interpolation: a PR
# title is attacker-controlled text, and interpolating it would paste
# it straight into this shell.
#
# This comment may not name the syntax it is warning about. A `run`
# block is interpolated whole before bash ever sees it, so a literal
# empty expression here is a *workflow* syntax error — the run is
# refused before any job is created, which is reported as a failure
# with no failing step and the file path where the name should be.
# That is what took CI down; `#` is a shell comment, not a shield.
{
printf '%s\n\n' "$PR_TITLE"
printf '%s\n' "$PR_BODY"
} > /tmp/pr_message.txt
bash tool/check/commits.sh --message /tmp/pr_message.txt

# The workflows themselves, because a broken one does not fail — it is
# *refused*. GitHub interpolates a `run` block whole before bash sees it,
# so an expression error anywhere in it, comment included, kills the run
Expand Down
25 changes: 16 additions & 9 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,39 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
# Flutter and Dart come from mise.toml, the same pin CI and every laptop
# use. Not subosito/flutter-action with `channel: stable`: that resolves
# to whatever stable is on the day, so this job could ship an artifact
# built against an SDK nobody chose, and nothing in the build log would
# say so. See AGENTS.md → Toolchain.
- name: Install toolchain (mise)
uses: jdx/mise-action@v4
Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · low
第三方 Action jdx/mise-action@v4 使用了版本標籤 (tag) 而非完整的提交雜湊值 (commit SHA)。雖然這比使用 channel: stable 更具決定性,但為了防止標籤被篡改(供應鏈攻擊),建議將其鎖定在特定的 commit SHA。

Suggestion:

Suggested change
- name: Install toolchain (mise)
uses: jdx/mise-action@v4
- name: Install toolchain (mise)
uses: jdx/mise-action@<commit_sha>

Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · medium
原本使用 subosito/flutter-action@v2 並開啟了 cache: true,這會自動快取 Flutter/Dart 的 pub cache。現在改用 jdx/mise-action@v4 後,若沒有額外透過 actions/cache 設定快取 ~/.pub-cache(或對應路徑),每次執行 tool/dev/deps.sh 都必須重新下載所有套件,這將會顯著增加建置時間。

Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
jdx/mise-action@v4 是一個第三方 Action,目前使用的是 tag (@v4) 而非完整的 Commit SHA。根據安全最佳實踐,建議將第三方 Action 鎖定在特定的 Commit SHA,以防止該 tag 被篡改或指向惡意代碼,從而降低供應鏈攻擊的風險。


# The iOS build uses Swift Package Manager, not CocoaPods (there is no
# Podfile). Cache SPM's clone/artifact store keyed on the resolved pins.
- name: Cache Swift Packages
uses: actions/cache@v4
with:
# build/ios/SourcePackages, not DerivedData. Flutter always passes
# -clonedSourcePackagesDirPath pointing there (ios/xcodeproj.dart), so
# DerivedData is never consulted for packages and caching it stored
# nothing. Losing this directory is what makes a launch print fifteen
# "Fetching from … (cached)" lines and cost 19-32 s.
path: |
~/Library/Caches/org.swift.swiftpm
~/Library/Developer/Xcode/DerivedData/**/SourcePackages
build/ios/SourcePackages
key: ${{ runner.os }}-spm-${{ hashFiles('ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-

- name: Install dependencies and run build_runner
run: |
flutter pub get
dart run build_runner build --delete-conflicting-outputs
bash tool/dev/deps.sh
bash tool/dev/codegen.sh

- name: Build iOS App and create IPA
run: |
flutter build ios --debug --no-codesign
bash tool/dev/build.sh ios --debug
mkdir -p Payload
cp -R build/ios/iphoneos/Runner.app Payload/Runner.app
zip -qr DPIP.ipa Payload
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,13 @@ jobs:
# for why a release accumulates across the snapshots it followed while a
# snapshot is only a delta.
- name: Release notes
env:
# Actions does not put GITHUB_TOKEN in the environment; a step has to
# ask. Without it notes.sh resolves authors unauthenticated, at 60
# requests an hour shared across everything on that runner's IP — and
# a rate-limited release produced a note that credited nobody and said
# nothing about why.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
bash tool/release/notes.sh \
Expand Down
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,12 @@ tool/check.sh

That is the whole list, and it is the same list `.github/workflows/ci.yml`
runs — CI calls these scripts rather than naming the commands itself, so the
two cannot drift. Individually, if you want to fail faster:
two cannot drift.

CI judges **the commits on the branch**, never the pull request's title or
description — which is safe because rebase is the only merge this repository
allows (Settings → Pull requests). What lands on main is what was judged.
See [commit.md](commit.md). Individually, if you want to fail faster:

```sh
tool/check/commits.sh origin/main..HEAD
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ tool/dev/build.sh ios # iOS(不含簽章)

## Star History

<a href="https://star-history.com/#exptechtw/dpip&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=exptechtw%2Fdpip&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=exptechtw%2Fdpip&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=exptechtw%2Fdpip&type=Date" />
</picture>
<a href="https://www.star-history.com/?type=date&repos=exptechtw%2Fdpip">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=exptechtw/dpip&type=date&theme=dark&legend=top-left&sealed_token=W5nrby1cW41C6wO-pyTS03g09KIf7gK4LQILagMeXzRWFPhswq_OAHgFdq_mKh-QcVxUvdyYzc-IcWOEL3m-QhUH49bzP85hsXEwi6x5YRj6R8QeRcndYw" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=exptechtw/dpip&type=date&legend=top-left&sealed_token=W5nrby1cW41C6wO-pyTS03g09KIf7gK4LQILagMeXzRWFPhswq_OAHgFdq_mKh-QcVxUvdyYzc-IcWOEL3m-QhUH49bzP85hsXEwi6x5YRj6R8QeRcndYw" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=exptechtw/dpip&type=date&legend=top-left&sealed_token=W5nrby1cW41C6wO-pyTS03g09KIf7gK4LQILagMeXzRWFPhswq_OAHgFdq_mKh-QcVxUvdyYzc-IcWOEL3m-QhUH49bzP85hsXEwi6x5YRj6R8QeRcndYw" />
</picture>
</a>
32 changes: 24 additions & 8 deletions commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,13 @@ ci: cache the Swift package resolution

每一項後面標上**真正寫它的人**,以及該則 commit 的連結。

歸屬不是取 commit 的 author:GitHub squash 一個 PR 時會把作者設成按下合併的人。
`41a3c1e8 Fix eew (#534)` 的作者是合併者,而它的每一行都是別人寫的。所以摘要帶
`(#N)` 時,作者取自**那個 PR 自己的 commits**,再併入 `Co-authored-by:` trailer;
都沒有才退回 commit 的 author。是 GitHub 帳號,不是 git 顯示名稱 —— 顯示名稱 @
不到任何人。
歸屬先看 `Co-authored-by:` trailer,沒有就拿 SHA 問 GitHub 這則 commit 的作者
帳號,再沒有才退回 git 的顯示名稱。要的是 GitHub 帳號,不是顯示名稱 —— 顯示名稱
@ 不到任何人。

只開放 rebase 合併,所以 main 上的 commit 就是有人寫的那則,作者就是作者。
**main 上已經用 squash 進來的 282 則不往回相容**:沒有 trailer 的那些會標成按下
合併的人。這是知情的取捨,不是疏漏 —— 下一份 note 裡的 `c5fdbd31` 就會這樣。

| | 涵蓋範圍 | 為什麼 |
|---|---|---|
Expand All @@ -369,9 +371,9 @@ ci: cache the Swift package resolution

實際長相就是上面各節的範例,`tool/release/notes.sh` 直接照這個格式輸出。

> **squash 會壓縮條目數。** 正則是逐行抓的,所以 squash 不會像舊格式那樣把內容
> 弄壞——但四則 commit 的條目會全部掛在同一個作者和同一個快照下。要保留就用
> rebase-merge
> **這裡只能 rebase 合併**,squash 和 merge commit 在 repo 設定裡都關掉了。
> 每則條目的平台圖示、作者、快照標記、commit 連結,四樣都是從那則 commit 的 SHA
> 推導的 —— squash 之後只剩一個 SHA,四樣就全部指向同一坨

---

Expand All @@ -395,6 +397,20 @@ git push --force-with-lease
pull request 會建一個分支併入 base 的合成 merge,而這個 gate 判的每一則都必須
是有人真的寫過的。

### gate 只看 commit,不看 PR 標題與描述

**PR 的標題和描述完全不在檢查範圍內。** 被判的是分支上每一則 commit ——
它們是有人真的寫過的東西,而標題預設是從分支名生出來的(`Fix/version week`),
描述可以是空的。

這樣是安全的,**前提是合併方式只有 rebase**,而那是 repo 設定裡強制的:
Settings → Pull requests 只勾了 `Allow rebase merging`。所以進 main 的就是被
gate 判過的那些 commit 本身,每一則也各自保留自己的條目、作者與快照標記。

把 squash 打開就會破壞這個前提 —— 進 main 的會變成標題加描述,而那兩樣沒有任何
東西在看。`332fb8f3 Fix report (#529)` 就是這樣進來的:不是任何人寫過的 commit,
而且改不掉了。

---

## 不合格怎麼辦
Expand Down
98 changes: 98 additions & 0 deletions test/tool/release_notes_wrap_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/// `tool/release/notes.sh` extracts changelog entries with a whole-line regex,
/// and commit.md's own example wraps an entry across two lines. Those two facts
/// disagreed, and the disagreement shipped: three English sentences in the
/// 26w34b notes end mid-clause — "…is shown at full extent on the monitor and
/// the" — because the indented remainder never matched anything.
///
/// Nothing failed. The note built, the release published, and the only symptom
/// was a sentence that stops.
library;

import 'dart:io';

import 'package:flutter_test/flutter_test.dart';

/// Runs the fold the extractor applies to a commit body.
///
/// Lifted from the script by reading it, so a change to the awk that stops
/// folding fails here rather than in a published release note.
String fold(String body) {
final script = File('${Directory.current.path}/tool/release/notes.sh')
.readAsStringSync();
final start = script.indexOf("git log -1 --format=%b \"\$sha\" | awk '");
expect(start, isNot(-1), reason: 'the fold is no longer where it was');
final awk = script.substring(
script.indexOf("'", start) + 1,
script.indexOf("')", start),
);
Comment on lines +22 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · high
test/tool/release_notes_wrap_test.dartfold 函數中,對 tool/release/notes.sh 的實作細節存在高度耦合。該函數透過 indexOf 搜尋特定的指令字串來提取 awk 腳本,這意味著一旦 notes.sh 的程式碼格式(如空格、引號或指令結構)發生任何非邏輯性的變動,測試將會因為找不到對應位置而失敗,導致測試非常脆弱(Brittle)。建議考慮將 awk 邏輯提取到獨立檔案,或者在測試中更穩健地定位該邏輯。

Comment on lines +22 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
fold 函式的實作方式過於脆弱(Brittle)。它使用 indexOf 並匹配一個非常具體的字串來定位並提取 awk 腳本。一旦 tool/release/notes.sh 中的命令格式(例如空格、引號或參數順序)發生任何微小變化,該測試就會因為找不到匹配字串而失效,增加了維護成本。建議改用更穩健的方式(例如使用正規表示式或更具彈性的字串搜尋)來定位 awk 腳本塊。


final tmp = File(
'${Directory.systemTemp.createTempSync('fold').path}/body.txt',
)..writeAsStringSync(body);
return Process.runSync('bash', [
'-c',
"awk ${_q(awk)} ${_q(tmp.path)}",
]).stdout
as String;
}

String _q(String s) => "'${s.replaceAll("'", r"'\''")}'";

/// The regex the script uses, verbatim.
bool isEntry(String line) => RegExp(
r'^(New|Optimization|Fix)\(([A-Za-z]{2,3}(-[A-Za-z0-9]+)*)\):\s*(.+)$',
).hasMatch(line);
Comment on lines +42 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
測試檔案中的 isEntry 函數使用了硬編碼的正規表示法(Regex)來驗證結果。若 tool/release/notes.sh 修改了其用來識別條目的正規表示法邏輯(例如增加新的類型或修改格式),但測試中的 isEntry 未同步更新,將會導致測試結果無法真實反映腳本的功能變化,進而造成誤判。建議 isEntry 的 Regex 應與 notes.sh 中的 LINE_RE 保持一致。


void main() {
test('a wrapped entry survives whole', () {
const body = '''
New(en-US): a large event is shown at full extent on the monitor and the
replay map
New(zh-Hant): 大事件在監視器與重播地圖上以完整範圍顯示
''';

final entries = fold(body).split('\n').where(isEntry).toList();

expect(entries, hasLength(2));
// The half that used to be dropped.
expect(entries.first, endsWith('and the replay map'));
});

test('an unwrapped entry is untouched', () {
const body = 'Fix(en-US): stop the crash\nFix(zh-Hant): 修正閃退\n';

expect(fold(body), body);
});

test('prose after an entry does not get folded into it', () {
// The body's explanation is not part of the entry. A blank line ends it,
// which is what separates a continuation from the next paragraph.
const body = '''
Fix(en-US): stop the crash

The crash came from a null channel.
''';

final entries = fold(body).split('\n').where(isEntry).toList();

expect(entries, hasLength(1));
expect(entries.single, 'Fix(en-US): stop the crash');
});

test('the released commit that exposed this now reads whole', () {
// 41a3c1e8 shipped three truncated sentences. If the fold regresses, this
// is the commit that will say so.
final body =
Process.runSync('git', ['log', '-1', '--format=%b', '41a3c1e8']).stdout
as String;
if (body.trim().isEmpty) return; // shallow clone
Comment on lines +85 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test · low
針對特定 Commit (41a3c1e8) 的回歸測試,在進行淺層複製(shallow clone)的 CI 環境中會因找不到該 Commit 而直接跳過 (if (body.trim().isEmpty) return;)。這可能導致關鍵的真實案例驗證未能執行。雖然這在某些情況下是合理的,但如果這是該測試的核心價值所在,建議考慮將該 Commit 的內容作為測試資料的一部分直接寫入測試腳本中,以確保 CI 環境下的測試一致性與有效性。


final entries = fold(body).split('\n').where(isEntry).toList();

expect(
entries.where((e) => e.endsWith(' and the')),
isEmpty,
reason: 'an entry still ends mid-clause',
);
});
}
25 changes: 25 additions & 0 deletions test/tool/run_script_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,31 @@ void main() {
}
});

test('the toolchain probe does not depend on a shell builtin', () {
// `mise exec` runs a binary, not a shell line. macOS ships
// /usr/bin/command as a real executable and Linux does not, so
// `mise exec -- command -v flutter` answered correctly on a laptop and
// answered nothing on every Linux runner — which the guard then reported as
// "the SDK is not installed". A probe that is wrong about the toolchain is
// worse than no probe.
// Code only. The comment above the fix has to be able to name the mistake
// it describes, the same way tool/check/tooling.sh lets prose spell out the
// command it bans.
final code = File('${Directory.current.path}/tool/dev/_lib.sh')
.readAsLinesSync()
.where((l) => !l.trimLeft().startsWith('#'))
.join('\n');

expect(code, contains('mise which flutter'));
for (final builtin in ['command -v', 'type -p', 'hash ']) {
expect(
code,
isNot(contains('mise exec -- $builtin')),
reason: '$builtin is a shell builtin; mise execs directly',
);
}
});

test('the git hooks point at a script that exists', () {
// The hooks have no file extension, so a rename sweep over `*.sh` misses
// them — and the failure is one line of shell noise on every commit that
Expand Down
Loading
Loading