From a3eff181f3bed136506ba9a20de4149e9fedd13f Mon Sep 17 00:00:00 2001 From: YuYu1015 Date: Tue, 18 Aug 2026 17:32:24 +0800 Subject: [PATCH 1/2] feat(release): announce a release on Discord MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optimization(zh-Hant): 發布新版本時自動發到 Discord Optimization(en-US): a new release is announced on Discord automatically The step lives in the release job rather than in a workflow listening on `release`, because GitHub refuses to start a run from an event its own GITHUB_TOKEN caused and `gh release create` uses exactly that token — an `on: release` workflow would sit silent forever and look like a Discord fault. Listening for both `published` and `prereleased` would also have announced every snapshot twice. A note is written for GitHub and does not fit Discord: 37,724 characters for 26w34c against a 4,096-character embed. The English block goes because it lives in `
`, which Discord does not render; the platform badges go because an embed renders no images and they would arrive as literal markdown; and the commit URLs go because keeping them costs 8,062 characters and the release page is linked at the bottom. When it still overflows, the short hashes go before any entry does — losing a hash costs a click, losing an entry means a change shipped and nobody was told. What is left is split evenly between the three categories, each saying how much it is holding back, because filling from the top spends the whole budget on 新功能 and posts 錯誤修正 as an empty heading. Announcement failure is a warning, not a red job: by then the build is published and the artifacts are downloadable, and failing the run would say the release did not happen. --- .github/workflows/release.yml | 31 +++ test/tool/discord_payload_test.dart | 107 +++++++++ test/tool/fixtures/release_26w34c.json | 116 ++++++++++ tool/release/discord.py | 309 +++++++++++++++++++++++++ 4 files changed, 563 insertions(+) create mode 100644 test/tool/discord_payload_test.dart create mode 100644 test/tool/fixtures/release_26w34c.json create mode 100755 tool/release/discord.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 922f48334..ee73f5c89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -365,6 +365,7 @@ jobs: cat NOTES.md - name: Publish + id: publish env: GH_TOKEN: ${{ github.token }} run: | @@ -386,3 +387,33 @@ jobs: --notes-file NOTES.md \ --prerelease="${{ needs.version.outputs.prerelease }}" \ artifacts/* + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + # Here rather than in a workflow of its own listening on `release`. + # GitHub refuses to start a workflow run from an event its own + # GITHUB_TOKEN caused, and the release above is created with exactly that + # token — an `on: release: [published]` workflow would sit silent forever + # and look like a Discord problem. + # + # `published` would also have been the only safe type to listen for: + # publishing a pre-release fires `published` *and* `prereleased`, so + # asking for both announces every snapshot twice. + - name: Announce on Discord + if: always() && steps.publish.outputs.tag != '' + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + DPIP_EMOJI_ANDROID: ${{ secrets.DPIP_EMOJI_ANDROID }} + DPIP_EMOJI_IOS: ${{ secrets.DPIP_EMOJI_IOS }} + # Without it the release lookup runs unauthenticated, at 60 requests + # an hour shared across everything on this runner's IP. + GITHUB_TOKEN: ${{ github.token }} + run: | + if [ -z "${DISCORD_WEBHOOK:-}" ]; then + echo "::notice::DISCORD_WEBHOOK is not set — nothing announced" + exit 0 + fi + # A warning, not a failure. The build is published and the artifacts + # are downloadable by the time this runs; turning the job red over an + # announcement would say the release did not happen. + python3 tool/release/discord.py "${{ steps.publish.outputs.tag }}" || + echo "::warning::Discord announcement failed — the release itself is fine" diff --git a/test/tool/discord_payload_test.dart b/test/tool/discord_payload_test.dart new file mode 100644 index 000000000..74171c443 --- /dev/null +++ b/test/tool/discord_payload_test.dart @@ -0,0 +1,107 @@ +/// `tool/release/discord.py` — the release announcement, inside Discord's +/// limits. +/// +/// The truncation path only runs on a release big enough to overflow, which is +/// exactly the release nobody wants to discover it on. These force the overflow +/// with a smaller budget so the behaviour is exercised on every run, and read a +/// committed fixture rather than the API so they are neither flaky nor +/// rate-limited. +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +const _android = '<:android:1539201870935892028>'; +const _ios = '<:ios:1539201897766981692>'; + +Map payload({int? limit, bool short = false}) { + final root = Directory.current.path; + final result = Process.runSync( + 'python3', + ['$root/tool/release/discord.py', '26w34c', '--dry-run'], + environment: { + 'DPIP_RELEASE_JSON': '$root/test/tool/fixtures/release_26w34c.json', + 'DPIP_EMOJI_ANDROID': short ? '<:a:1>' : _android, + 'DPIP_EMOJI_IOS': short ? '<:i:2>' : _ios, + if (limit != null) 'DPIP_DISCORD_LIMIT': '$limit', + }, + ); + expect(result.exitCode, 0, reason: result.stderr.toString()); + return jsonDecode(result.stdout as String) as Map; +} + +List embeds(Map p) => p['embeds']! as List; +String description(Map p) => + (embeds(p).single as Map)['description']! as String; + +/// `… (15/22)` → 15. +int shown(String text) => + RegExp(r'… \((\d+)/\d+\)') + .allMatches(text) + .map((m) => int.parse(m.group(1)!)) + .fold(0, (a, b) => a + b); + +void main() { + test('one embed, always inside the description limit', () { + for (final limit in [4096, 3000, 2000, 1200]) { + final p = payload(limit: limit); + expect(embeds(p), hasLength(1), reason: 'limit $limit'); + expect( + description(p).length, + lessThanOrEqualTo(limit), + reason: 'limit $limit', + ); + } + }); + + test('the whole note when it fits, nothing marked missing', () { + // Short ids stand in for a guild whose emoji names are brief; the note then + // fits and no category should claim to be cut. Shortening only one of the + // two still overflows — 55 entries pay for both on almost every line. + final text = description(payload(short: true)); + + expect(text, isNot(contains('… ('))); + expect(text, contains('修正換行的更新日誌條目在發布時被從中間截斷')); + }); + + test('hashes are dropped before any entry is', () { + // The order matters: a hash costs a click on the link at the bottom, a + // dropped entry means a change shipped and nobody was told. + final full = description(payload(short: true)); + final tight = description(payload()); + + expect(full, contains('`c5fdbd31`')); + expect(tight, isNot(contains('`c5fdbd31`'))); + }); + + test('a cut is split evenly between the categories', () { + // Filling from the top until the budget runs out would spend it all on + // 新功能 and post 錯誤修正 as an empty heading — a release where the fixes + // are the point would read as a release with no fixes. + final text = description(payload(limit: 1500)); + final counts = RegExp(r'… \((\d+)/(\d+)\)') + .allMatches(text) + .map((m) => int.parse(m.group(1)!)) + .toList(); + + expect(counts, hasLength(greaterThanOrEqualTo(2))); + expect(counts.toSet(), hasLength(1), reason: 'categories got $counts'); + }); + + test('a smaller budget never shows more', () { + expect( + shown(description(payload(limit: 1200))), + lessThanOrEqualTo(shown(description(payload(limit: 2000)))), + ); + }); + + test('the heading, the subtext and the link all survive a cut', () { + final text = description(payload(limit: 1200)); + + expect(text, startsWith('# 26w34c')); + expect(text, contains('-# 快照,取自 main 的')); + expect(text, contains('releases/tag/26w34c')); + }); +} diff --git a/test/tool/fixtures/release_26w34c.json b/test/tool/fixtures/release_26w34c.json new file mode 100644 index 000000000..b871c77a3 --- /dev/null +++ b/test/tool/fixtures/release_26w34c.json @@ -0,0 +1,116 @@ +{ + "url": "https://api.github.com/repos/ExpTechTW/DPIP/releases/372212282", + "assets_url": "https://api.github.com/repos/ExpTechTW/DPIP/releases/372212282/assets", + "upload_url": "https://uploads.github.com/repos/ExpTechTW/DPIP/releases/372212282/assets{?name,label}", + "html_url": "https://github.com/ExpTechTW/DPIP/releases/tag/26w34c", + "id": 372212282, + "author": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "node_id": "RE_kwDOKDMUEs4WL4I6", + "tag_name": "26w34c", + "target_commitish": "main", + "name": "26w34c", + "draft": false, + "immutable": false, + "prerelease": true, + "created_at": "2026-08-18T08:49:05Z", + "updated_at": "2026-08-18T09:00:47Z", + "published_at": "2026-08-18T09:00:47Z", + "assets": [ + { + "url": "https://api.github.com/repos/ExpTechTW/DPIP/releases/assets/519201062", + "id": 519201062, + "node_id": "RA_kwDOKDMUEs4e8mEm", + "name": "DPIP-26w34c.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 40205149, + "digest": "sha256:13fd7d459bcbfc6a2169ede0a9ef6c98d0b8207652e252764f1e7fe0506b785c", + "download_count": 0, + "created_at": "2026-08-18T09:00:46Z", + "updated_at": "2026-08-18T09:00:47Z", + "browser_download_url": "https://github.com/ExpTechTW/DPIP/releases/download/26w34c/DPIP-26w34c.apk" + }, + { + "url": "https://api.github.com/repos/ExpTechTW/DPIP/releases/assets/519201063", + "id": 519201063, + "node_id": "RA_kwDOKDMUEs4e8mEn", + "name": "DPIP-26w34c.ipa", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 34157959, + "digest": "sha256:396f2ef33a11b4bda8cbd83f2ff510b535f378f74f079d16e2364d33d8abdf8a", + "download_count": 0, + "created_at": "2026-08-18T09:00:46Z", + "updated_at": "2026-08-18T09:00:47Z", + "browser_download_url": "https://github.com/ExpTechTW/DPIP/releases/download/26w34c/DPIP-26w34c.ipa" + } + ], + "tarball_url": "https://api.github.com/repos/ExpTechTW/DPIP/tarball/26w34c", + "zipball_url": "https://api.github.com/repos/ExpTechTW/DPIP/zipball/26w34c", + "body": "_快照,取自 main 的 `7157f72c`。未經審查,可能有問題。_\n\n### 🌟 新功能\n\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 更多頁上方卡片重新設計,版本以漸層數字顯示,pre-release 顯示上一版主版本號 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 「更多」頁版本卡改以版號 26.1 為主,測試版同時標示完整版號 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 更多頁英雄區加入伺服器狀態卡,並縮小版本卡 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 版本卡片正式版也會顯示完整版本號 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 新版號 26.2.1 的版型只顯示 26.2 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 版本卡片徽章加上 build 日期 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 版本徽章右側顯示 build 日期 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 首頁地區列下方浮一條金色支持橫條,點擊前往贊助頁 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 繁體贊助相關文案改用「支援」 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) README 新增 Android 測試版與 iOS TestFlight 連結 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 更新日誌每張版本卡片下方新增 Contributors 頭像列 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修復伺服器狀態頁依賴的網路層支援 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 除錯版偵測到非腳本啟動將直接拒絕執行並提示正確指令,新增 Windows 啟動腳本 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 取得 App 下方新增 測試版 與 合作夥伴 區塊 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 伺服器狀態查詢在離線時會改用上次快取結果 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 更新日誌的貢獻者改為頭像加名稱的名牌,可點開 GitHub 個人頁 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 地震報告圖快取到本機,重複查看不再每次重新下載 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 無背景定位支援的平台不再回報麵包屑傾倒錯誤 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 除錯資訊頁新增「傾印除錯資訊及日誌」,上傳後複製連結 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 更新日誌每個版本卡片都可以開啟 GitHub 頁面 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 伺服器狀態頁新增 Cloudflare 各地區狀態與本機對多活端點的連線判斷表格 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 版本卡片可以深入檢視本次更新的內容與技術細節 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n\n### 🔌 最佳化\n\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 贊助卡片改為橫式排版,與右欄其他卡片對齊更好看 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) App 日誌改用內建畫面,多了搜尋、等級篩選與分享 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 日誌的等級標籤改成大寫,一眼看得出哪一行不是 INFO — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 主控台的日誌改成一則一行,不再有框線與跑掉的顏色碼 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 主控台的等級標籤可以上色,用 --dart-define=DPIP_LOG_COLOR=true 開啟 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 新增 tool/colorize_logs.sh,在終端機端替日誌上色 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 新增 tool/run.sh,一個指令跑起來就有上色的日誌 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 提交前的檢查改成只跑有變動的部分,沒改東西時約 1 秒跑完 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 沒有 mise 或用錯 SDK 時直接拒絕執行,不再無聲建置出不一樣的東西 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n\n### 🐞 錯誤修正\n\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正週一上午建置的版本號會標成上一週 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正圖層時間軸在某些時區會顯示成 UTC 時間 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正「更多」頁上方三張卡片的圖示與文字起點對齊 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正公測使用者可能被當成正式版使用者,收到比手上還舊的更新提示 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正在「更多」點開 App 日誌後畫面狂刷並卡死 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正日誌頁把同一個錯誤不斷印到主控台,回放的紀錄也不再遺失等級 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正開啟日誌頁會把整份紀錄重印到主控台,並重複寫回資料庫 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正日誌頁的清除只清畫面,重開又整份回來 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正日誌頁上方等級篩選器把回放的紀錄全算成 undefined — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正開啟日誌頁會把本次啟動的紀錄擠掉,順序也錯亂 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正啟動早期的紀錄從來沒被存下來,當機時最該看的那幾行都不見 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正 iOS 上開啟主控台顏色只會多出跳脫字元,看不到顏色 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正結束執行時出現 Broken pipe 的未處理例外 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 更新日誌改為標示真正寫這項變更的人,並附上可點擊的 commit 連結 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正開啟日誌頁動作選單時,終端機與日誌被大量錯誤洗版 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) run.sh 啟動時清掉舊的 DevFS 殘留,並修正清除快取在 debug 會刪掉執行中核心 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 伺服器狀態的本機狀態不再把不存在的服務當成未探測,改以圖例區分 正常/異常/未探測/不支援,並標明資料來自被動探測 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正每次啟動都要重新解析 Swift Package,啟動快約 12 秒 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 提交與推送前的檢查改由 git hook 強制執行,不再只是印出來 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) CI 不再檢查 PR 標題與描述,只檢查分支上每一則 commit — @whes1015 ([`dec7fc6b`](https://github.com/ExpTechTW/DPIP/commit/dec7fc6b3ba655b900db4ad33b98a878ea35ca74))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正 Linux 上會誤判「SDK 未安裝」而拒絕執行 — @whes1015 ([`6d933bb8`](https://github.com/ExpTechTW/DPIP/commit/6d933bb8756053261f17bd40191a01abb8030d1b))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) Android 與 iOS 的建置改用釘選的 Flutter 版本,不再跟著當日 stable 走 — @whes1015 ([`063d978d`](https://github.com/ExpTechTW/DPIP/commit/063d978d1a726f1cfec23a3d2af876f04c6a6c82))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正更新日誌的署名會在超過 30 則 commit 或被限流時無聲消失 — @whes1015 ([`6d2295f0`](https://github.com/ExpTechTW/DPIP/commit/6d2295f0f455c8c35c0a83fc341e11b5c5df71c6))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) 修正換行的更新日誌條目在發布時被從中間截斷 — @whes1015 ([`e26b445e`](https://github.com/ExpTechTW/DPIP/commit/e26b445e580f8e5ce6b8c36ecbef670245ff139b))\n\n\n\n
\nEnglish\n\n### 🌟 New features\n\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the More hero cards go flat, and the version card leads with a gradient major.minor named after the last release — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the version card on the More page leads with 26.1 and shows the full snapshot name beneath it — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) Add the server-status card to the More hero block and slim the version card — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) The hero version card now shows a version line for releases too — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) A three-part release (26.2.1) advertises as 26.2; the patch stays in the label — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) The More page badge shows the day the build was cut — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) The version badge now carries the day this build was cut — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) Home floats a gold support pill under the region bar, opening the sponsor page — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) Traditional-Chinese support copy now reads 支援, not 支持 — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) README now links the Android testing track and the iOS TestFlight beta — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) changelog cards foot a Contributors avatar strip — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) restore the ApiClient hooks the status page relies on — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the debug build refuses non-script launches and there is now a Windows run script — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) beta channels and partner tiles now sit under Get the app — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) status POSTs cache under their URL and serve offline — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) changelog contributors are avatar + name badges that open GitHub — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the report image now round-trips the ETag cache — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) breadcrumb draining no longer trips on missing plugins — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the debug page can upload the diagnostics and log, and copies the link back — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) every changelog card foots a link to its release on GitHub — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the server status page adds Cloudflare regional status and the device's own view of each multi-active endpoint — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the version card opens this release's highlights and deep dives — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n\n### 🔌 Improvements\n\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the sponsor card now row-aligns with the right column — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the log screen gains search, level filtering and sharing — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) log lines are tagged INFO / WARN / ERROR, readable at a glance — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) console logs are one plain line each, without borders or stray colour codes — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the console level tag can be coloured with --dart-define=DPIP_LOG_COLOR=true — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) add tool/colorize_logs.sh, which colours the log in the terminal — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) add tool/run.sh so one command runs the app with a coloured log — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the pre-commit checks only re-run what changed, about a second on an unchanged tree — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the build now refuses to start off the wrong SDK instead of using it silently — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n\n### 🐞 Bug fixes\n\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) fix a Monday-morning build being named for the previous week — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) map timelines now show frame times in the device's local time instead of UTC — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) align the icons and text of the three hero cards on the More page — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) fix a beta tester being treated as a stable user and offered an older build — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) fix the app freezing after opening the log screen from More — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) stop the log screen reprinting one fault forever, and keep replayed levels — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) opening the log no longer reprints the whole table or writes it back — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) clearing the log no longer leaves everything to come back on reopen — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the level filter counts replayed lines under their level again — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) opening the log no longer pushes out the running session's lines — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) fix the launch's own log lines never being stored, the ones a crash needs — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) enabling console colour on iOS added escape characters instead of colour — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) fix the unhandled Broken pipe exception when a run is stopped — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) each entry credits who wrote it and links the commit it came from — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) opening the log screen's Actions sheet no longer floods the log — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) run.sh sweeps stale DevFS dirs, and clear-cache no longer wipes the live kernel — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the local probe tables now tell unsupported services apart from merely-unprobed ones and note that the data is passively observed — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the Swift Package graph no longer re-resolves on every launch — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the pre-commit checks are now enforced by git hooks instead of only printed — @whes1015 ([`c5fdbd31`](https://github.com/ExpTechTW/DPIP/commit/c5fdbd31a242f6c57cb98731e473cab37d0b1b94))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) CI no longer checks the PR title and body, only the commits on the branch — @whes1015 ([`dec7fc6b`](https://github.com/ExpTechTW/DPIP/commit/dec7fc6b3ba655b900db4ad33b98a878ea35ca74))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) fix the toolchain guard wrongly refusing to run on Linux — @whes1015 ([`6d933bb8`](https://github.com/ExpTechTW/DPIP/commit/6d933bb8756053261f17bd40191a01abb8030d1b))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) the Android and iOS builds now use the pinned Flutter, not whatever stable is today — @whes1015 ([`063d978d`](https://github.com/ExpTechTW/DPIP/commit/063d978d1a726f1cfec23a3d2af876f04c6a6c82))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) release-note attribution no longer disappears silently past 30 commits or under a rate limit — @whes1015 ([`6d2295f0`](https://github.com/ExpTechTW/DPIP/commit/6d2295f0f455c8c35c0a83fc341e11b5c5df71c6))\n- ![Android](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/android.svg) ![iOS](https://raw.githubusercontent.com/ExpTechTW/DPIP/main/.github/assets/ios.svg) a wrapped changelog entry is no longer published cut off mid-sentence — @whes1015 ([`e26b445e`](https://github.com/ExpTechTW/DPIP/commit/e26b445e580f8e5ce6b8c36ecbef670245ff139b))\n\n
\n\n\n\n\n\n", + "mentions_count": 1 +} diff --git a/tool/release/discord.py b/tool/release/discord.py new file mode 100755 index 000000000..28b147c23 --- /dev/null +++ b/tool/release/discord.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +"""Posts a release's changelog to Discord. + + DISCORD_WEBHOOK=... tool/release/discord.py 26w34c + DISCORD_WEBHOOK=... tool/release/discord.py 26w34c --dry-run + +The release note is written for GitHub and has to be cut down before Discord +will take it: 37,724 characters for 26w34c against a 4,096-character limit on an +embed description and 6,000 across a whole message. + +Three things account for almost all of it, and each is dropped for its own +reason rather than to save space: + + * the English block, which lives inside `
` — Discord does not render + that tag at all, so it would arrive as a wall of unfolded duplicate text; + * the platform badges, which are `![Android](https://…svg)` image links — + Discord renders no images inside a description, so they would arrive as + literal markdown. 🤖 and 🍎 say the same thing in two characters; + * the commit links. The short hash stays, the URL goes: keeping both costs + 8,062 characters and does not fit, keeping the hash alone costs 3,277 and + does, and the release page linked at the end is one click from every commit. +""" + +import json +import os +import pathlib +import re +import sys +import time +import urllib.error +import urllib.request + +REPO = os.environ.get("GITHUB_REPOSITORY", "ExpTechTW/DPIP") +DESCRIPTION_LIMIT = int(os.environ.get("DPIP_DISCORD_LIMIT", 4096)) + +# Pre-releases are the everyday case here and releases are the rare one, so the +# colour is what tells them apart at a glance in a busy channel. +PRERELEASE_COLOUR = 0xE8A33D +RELEASE_COLOUR = 0x3BA55D + +# The platform marks. A guild's own emoji only render from their numeric id — +# `:android:` stays literal text in a webhook message — and that id cannot be +# read through a webhook token (guilds/{id}/emojis answers 401), so it is +# configuration rather than something this script can discover. Falling back to +# the unicode pair keeps a release note posting on a server that has no custom +# emoji, which is every server but one. +ANDROID = os.environ.get("DPIP_EMOJI_ANDROID", "🤖") +IOS = os.environ.get("DPIP_EMOJI_IOS", "🍎") + + +def api(path: str) -> dict: + """A release, read back from the API. + + Retried on 404 because this runs seconds after `gh release create` and the + API is not read-your-writes: a release that certainly exists can answer 404 + for a moment, and the announcement would be lost to a race nobody could + reproduce afterwards. + """ + fixture = os.environ.get("DPIP_RELEASE_JSON") + if fixture: + return json.loads(pathlib.Path(fixture).read_text()) + + for attempt in range(3): + try: + return _get(path) + except urllib.error.HTTPError as error: + if error.code != 404 or attempt == 2: + raise + time.sleep(2 * (attempt + 1)) + raise AssertionError("unreachable") + + +def _get(path: str) -> dict: + request = urllib.request.Request( + f"https://api.github.com/repos/{REPO}/{path}", + headers={"Accept": "application/vnd.github+json"}, + ) + token = os.environ.get("GITHUB_TOKEN") + if token: + request.add_header("Authorization", f"Bearer {token}") + with urllib.request.urlopen(request, timeout=20) as response: + return json.load(response) + + +def to_discord(body: str) -> str: + """The Chinese half of a release note, in what Discord can actually show.""" + body = body.split("", "", body, flags=re.S) + return re.sub(r"\n{3,}", "\n\n", body).strip() + + + + +def sections(body: str) -> list[tuple[str, list[str]]]: + """The note split at its category headings, each as a list of entries.""" + out: list[tuple[str, list[str]]] = [] + for block in re.split(r"(?=^### )", body, flags=re.M): + block = block.strip() + if not block: + continue + title, _, rest = ( + block.partition("\n") if block.startswith("### ") else ("", "", block) + ) + items = [ln for ln in rest.strip().splitlines() if ln.strip()] + out.append((title[4:].strip(), items)) + return out + + +def share(parts: list[tuple[str, list[str]]], room: int) -> list[list[str]]: + """Splits [room] between the categories, evenly, and without waste. + + Evenly, because the alternative is what a naive cut does: fill from the top + until the budget runs out, which spends everything on 新功能 and posts + 錯誤修正 as an empty heading — and a release where the fixes are the point + then reads as a release with no fixes. + + Without waste, because an equal share is a floor and not a quota: a + category that needs less than its share hands the rest back, and the loop + repeats until nothing more can be given away. Only then is anything cut. + """ + keep: list[list[str]] = [[] for _ in parts] + left = room + + # Round by round, an equal quota each, until a whole round adds nothing. + # + # Stopping when no category *completes* is what the first version did, and + # it left 799 characters unspent while dropping eight entries: the two long + # categories were both still hungry, so neither finished, so the loop gave + # up holding a budget it could have spent. Progress is the condition, not + # completion. + while left > 0: + hungry = [i for i in range(len(parts)) if len(keep[i]) < len(parts[i][1])] + if not hungry: + break + quota = max(left // len(hungry), 1) + spent = 0 + for i in hungry: + used = 0 + for item in parts[i][1][len(keep[i]) :]: + if used + len(item) + 1 > quota: + break + used += len(item) + 1 + keep[i].append(item) + spent += used + if spent == 0: + break + left -= spent + return keep + + +def fit(text: str, room: int) -> str: + """The per-embed backstop: [share] bounds the message, this bounds one embed. + + Cut on an item boundary. A description cut at the character limit ends + inside somebody's changelog entry, which reads as a bug in that entry + rather than as a limit in Discord. + """ + if len(text) <= room: + return text + cut = text.rfind("\n", 0, room - 20) + return text[: cut if cut > 0 else room - 20].rstrip() + "\n…" + + +def budget(body: str, hashes: bool) -> str: + """Drops the short commit hashes, from entries only. + + Line by line rather than over the whole note: the snapshot notice at the top + names the commit it was cut from, and a blanket substitution ate it — + "取自 main 的。未經審查" is a sentence with its subject removed. + """ + if hashes: + return body + return "\n".join( + re.sub(r"\s*`[0-9a-f]{7,8}`", "", line) if line.startswith("- ") else line + for line in body.splitlines() + ) + + +def main() -> int: + args = [a for a in sys.argv[1:] if not a.startswith("--")] + dry_run = "--dry-run" in sys.argv + if not args: + print("usage: tool/release/discord.py [--dry-run]", file=sys.stderr) + return 2 + tag = args[0] + + release = api(f"releases/tags/{tag}") + url = release["html_url"] + link = f"\n\n[在 GitHub 上檢視完整更新日誌 →]({url})" + + + colour = PRERELEASE_COLOUR if release["prerelease"] else RELEASE_COLOUR + body = to_discord(release["body"]) + + # One embed per category, because a whole note does not fit in one: an + # embed description stops at 4,096 characters and a message at 6,000, and + # 26w34c needs 6,231 once the guild's platform emoji are in (they cost 58 + # characters an entry against 2 for the unicode pair). + # + # When it still will not fit, the short commit hashes go before any entry + # does. Losing a hash costs a click on the release page that is linked at + # the bottom anyway; losing an entry means a change shipped and nobody was + # told, which is the one thing this message exists to prevent. + # The hashes go before any entry does. Losing a hash costs a click on the + # release page linked at the bottom; losing an entry means a change shipped + # and nobody was told, which is the one thing this message exists to + # prevent. `26w34c` needs 6,231 characters with the guild's platform emoji + # in (58 an entry against 2 for the unicode pair) and 5,651 without hashes. + title_text = release["name"] or tag + footer = "測試版" if release["prerelease"] else "正式版" + + # Everything Discord counts that is not an entry, measured rather than + # guessed. A round reservation was 400 and the true figure is under 300, and + # the difference came straight out of the changelog: 26w34c lost its last + # two fixes to a number nobody had checked. + heading = f"# {title_text}" + + for hashes in (True, False): + parts = sections(budget(body, hashes)) + intro = " ".join(parts[0][1]).strip("_ ") if parts and not parts[0][0] else "" + header = f"{heading}\n-# {intro}" if intro else heading + if intro: + parts = parts[1:] + + # One embed, so the ceiling is a description's 4,096 and not a message's + # 6,000. Measured from the strings rather than reserved as a round + # number: a guess of 400 against a true 240 came straight out of the + # changelog, and nobody would have known which entries it cost. + overhead = ( + len(header) + + len(link) + + sum(len(f"\n\n### {name}\n") for name, _ in parts) + ) + room = DESCRIPTION_LIMIT - overhead + if sum(len(i) + 1 for _, items in parts for i in items) <= room: + break + + kept = share(parts, room) + + blocks = [header] + for (name, items), shown in zip(parts, kept): + block = f"### {name}\n" + "\n".join(shown) + if len(shown) < len(items): + # Says how much is missing, per category. A silent cut reads as a + # quiet release rather than a long one. + block += f"\n… ({len(shown)}/{len(items)})" + blocks.append(block) + + embeds = [ + { + "description": fit("\n\n".join(blocks), DESCRIPTION_LIMIT - len(link)) + + link, + "color": colour, + "footer": {"text": footer}, + "timestamp": release["published_at"], + } + ] + + payload = {"username": "DPIP", "embeds": embeds} + + if dry_run: + print(json.dumps(payload, ensure_ascii=False, indent=2)) + shown = sum(len(k) for k in kept) + total = sum(len(items) for _, items in parts) + print( + f"\n描述 {len(embeds[0]['description'])} / {DESCRIPTION_LIMIT}" + f",條目 {shown}/{total},commit hash {'保留' if hashes else '捨棄'}", + file=sys.stderr, + ) + return 0 + + webhook = os.environ.get("DISCORD_WEBHOOK") + if not webhook: + print("DISCORD_WEBHOOK is not set", file=sys.stderr) + return 1 + + request = urllib.request.Request( + webhook, + data=json.dumps(payload).encode(), + headers={ + "Content-Type": "application/json", + # Discord answers 403 to the default `Python-urllib/3.x`, with no + # body and nothing that says why. Any other agent string is fine. + "User-Agent": f"DPIP-release-notifier (+https://github.com/{REPO})", + }, + ) + try: + with urllib.request.urlopen(request, timeout=20) as response: + print(f"discord: HTTP {response.status}") + return 0 + except urllib.error.HTTPError as error: + # Discord says what it disliked in the body; the exception alone says + # only the number, and a stack trace here names urllib rather than the + # field it rejected. + print(f"discord: HTTP {error.code}", file=sys.stderr) + print(error.read().decode(errors="replace")[:800], file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 7b0eef2457ac4ace7fbd8968755985ca04cacd75 Mon Sep 17 00:00:00 2001 From: YuYu1015 Date: Tue, 18 Aug 2026 17:35:46 +0800 Subject: [PATCH 2/2] ci(release): hardcode the Discord emoji ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A custom emoji's id is visible to everyone who can see the server, and useless without the webhook that is not — so it was a secret for no reason, and one more thing to configure before the announcement worked at all. In the workflow rather than as defaults in the script: a fork announcing to its own server then falls back to the unicode pair instead of referencing emoji it does not have. --- .github/workflows/release.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee73f5c89..be6c96b9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -402,8 +402,13 @@ jobs: if: always() && steps.publish.outputs.tag != '' env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} - DPIP_EMOJI_ANDROID: ${{ secrets.DPIP_EMOJI_ANDROID }} - DPIP_EMOJI_IOS: ${{ secrets.DPIP_EMOJI_IOS }} + # Not secrets: a custom emoji's id is visible to everyone who can see + # the server, and it is useless without the webhook that is. Here + # rather than as defaults in the script, so a fork announcing to its + # own server falls back to the unicode pair instead of referencing + # emoji it does not have. + DPIP_EMOJI_ANDROID: "<:android:1539201870935892028>" + DPIP_EMOJI_IOS: "<:ios:1539201897766981692>" # Without it the release lookup runs unauthenticated, at 60 requests # an hour shared across everything on this runner's IP. GITHUB_TOKEN: ${{ github.token }}