diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 922f48334..be6c96b9d 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,38 @@ 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 }} + # 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 }} + 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())