fix(server): woken snoozed threads get a fresh auto-settle window #52297
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.event.pull_request.number || github.sha }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| # Lint, typecheck, and the desktop build each get a runner so they run side by | |
| # side instead of one after another. The `Check` job at the end of this file | |
| # passes only when every other job does. | |
| lint: | |
| name: Lint | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - name: Reject repository-owned PR assets | |
| run: | | |
| files="$(git ls-files .github/pr-assets)" | |
| if test -n "$files"; then | |
| printf 'PR evidence must be uploaded to GitHub, not committed:\n%s\n' "$files" >&2 | |
| exit 1 | |
| fi | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version-file: package.json | |
| cache: true | |
| run-install: true | |
| - name: Ensure Electron runtime is installed | |
| run: vp run --filter @t3tools/desktop ensure:electron | |
| # Files/dependencies are repo-wide; export checks cover clean workspaces only. | |
| - name: Check unused code | |
| run: vp run knip:check | |
| - name: Check | |
| run: vp check | |
| typecheck: | |
| name: Typecheck | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version-file: package.json | |
| cache: true | |
| run-install: true | |
| - name: Ensure Electron runtime is installed | |
| run: vp run --filter @t3tools/desktop ensure:electron | |
| - name: Typecheck | |
| run: vpr typecheck | |
| build: | |
| name: Build | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - uses: ./.github/actions/setup-apt-mirrors | |
| # Runs in the background while Vite+ installs; the step before the build waits for it. | |
| - name: Start installing browser secret helper build libraries | |
| run: | | |
| sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources | |
| ( | |
| set +e | |
| sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config | |
| echo "$?" > "$RUNNER_TEMP/apt-status" | |
| ) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null & | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version-file: package.json | |
| cache: true | |
| run-install: true | |
| - name: Ensure Electron runtime is installed | |
| run: vp run --filter @t3tools/desktop ensure:electron | |
| - name: Finish installing browser secret helper build libraries | |
| timeout-minutes: 5 | |
| run: | | |
| until test -s "$RUNNER_TEMP/apt-status"; do sleep 1; done | |
| cat "$RUNNER_TEMP/apt.log" | |
| exit "$(cat "$RUNNER_TEMP/apt-status")" | |
| - name: Build desktop pipeline | |
| run: vp run build:desktop | |
| - name: Verify preload bundle output | |
| run: node apps/desktop/scripts/verify-preload-bundle.mjs | |
| # Everything except `t3` (apps/server) and the web app, which has its own job | |
| # below. `--parallel` drops the package dependency ordering that `vp run` | |
| # applies by default: these `test` tasks declare no `dependsOn` and resolve | |
| # workspace deps from source, so ordering only bought us idle runners between | |
| # dependency layers. The concurrency limit stays at the default 4 so peak load | |
| # per runner is unchanged. | |
| test: | |
| name: Test | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| # Blacksmith boots GitHub's Ubuntu runner image (gcc is usually present), | |
| # but ACP process-tree live tests compile a small pthread fixture with `cc` | |
| # and soft-skip when it is missing. Install build-essential so that path | |
| # always runs in CI instead of silently no-oping. | |
| - name: Install C toolchain for process-tree fixtures | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends build-essential | |
| command -v cc | |
| - uses: ./.github/actions/setup-apt-mirrors | |
| # The desktop tests compile the browser secret helper. The install runs in | |
| # the background while Vite+ installs; the step before the tests waits for it. | |
| - name: Start installing browser secret helper build libraries | |
| run: | | |
| sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources | |
| ( | |
| set +e | |
| sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config | |
| echo "$?" > "$RUNNER_TEMP/apt-status" | |
| ) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null & | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version-file: package.json | |
| cache: true | |
| run-install: true | |
| - name: Ensure Electron runtime is installed | |
| run: vp run --filter @t3tools/desktop ensure:electron | |
| - name: Test preview artifact validation | |
| run: python3 -B .github/scripts/stage-preview-bundle.test.py | |
| - name: Test nightly release checks | |
| run: node --test .github/scripts/check-nightly-release.test.cjs | |
| - name: Finish installing browser secret helper build libraries | |
| timeout-minutes: 5 | |
| run: | | |
| until test -s "$RUNNER_TEMP/apt-status"; do sleep 1; done | |
| cat "$RUNNER_TEMP/apt.log" | |
| exit "$(cat "$RUNNER_TEMP/apt-status")" | |
| - name: Test | |
| run: vp run --parallel --concurrency-limit 4 --filter '!t3' --filter '!@t3tools/monorepo' --filter '!@t3tools/web' test | |
| # The web suite is the largest outside the server. In the shared Test job it | |
| # started last and ran alone for about 40s after everything else finished. | |
| test_web: | |
| name: Test Web | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version-file: package.json | |
| cache: true | |
| run-install: true | |
| - name: Test | |
| run: vp run --filter @t3tools/web test | |
| # apps/server sets `fileParallelism: false`, so its test files run strictly | |
| # one at a time. Sharding spreads them over separate runners instead of | |
| # separate workers, so no two server test files ever share a machine and the | |
| # isolation that flag buys is preserved exactly. One file at a time keeps | |
| # about 1.5 cores busy, so 4 vCPU runners lose nothing against 8. The shards | |
| # split files by recorded duration (apps/server/src/testUtils/weightedShardSequencer.ts). | |
| test_server: | |
| name: Test Server ${{ matrix.shard }} | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4, 5, 6] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version-file: package.json | |
| cache: true | |
| run-install: true | |
| # No Electron setup here: `t3` (apps/server) has no Electron dependency | |
| # and none of its tests touch the runtime. Only the non-server `test` | |
| # job, which covers @t3tools/desktop, needs the download. | |
| - name: Test | |
| env: | |
| T3CODE_TRANSFER_BUDGET_REPORT_PATH: ${{ runner.temp }}/t3code-transfer-budget.md | |
| T3CODE_TRANSFER_BUDGET_RESULT_PATH: ${{ runner.temp }}/thread-transfer-result.json | |
| run: vp run --filter t3 test --shard ${{ matrix.shard }}/${{ strategy.job-total }} | |
| # integration/transferBudgetV2.integration.test.ts writes the report; exactly one shard | |
| # produces these files. Gating the upload on their presence keeps a | |
| # single `thread-transfer-results` artifact per run, which is the name | |
| # thread-transfer-report.yml resolves. | |
| - name: Detect transfer budget report | |
| id: transfer_budget | |
| if: always() | |
| run: | | |
| if test -f "${{ runner.temp }}/thread-transfer-result.json"; then | |
| echo "present=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "present=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish transfer budget report | |
| if: always() && steps.transfer_budget.outputs.present == 'true' | |
| run: | | |
| if test -f "${{ runner.temp }}/t3code-transfer-budget.md"; then | |
| tee -a "$GITHUB_STEP_SUMMARY" < "${{ runner.temp }}/t3code-transfer-budget.md" | |
| else | |
| echo "Transfer budget report was not produced." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Upload thread transfer result | |
| if: always() && steps.transfer_budget.outputs.present == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: thread-transfer-results | |
| path: ${{ runner.temp }}/thread-transfer-result.json | |
| if-no-files-found: ignore | |
| retention-days: 30 | |
| transfer-report: | |
| name: Transfer report artifact | |
| needs: test_server | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Require transfer measurements | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: thread-transfer-results | |
| path: ${{ runner.temp }}/thread-transfer | |
| # Split out of Check and Test: both paid ~7-9s to install a Rust toolchain | |
| # for checks that take under 3s, on the critical path of every PR. | |
| rust: | |
| name: Rust | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt | |
| - name: Check Rust formatting | |
| run: | | |
| for crate in resource-monitor kde-snap-shot hyprland-snap-shot; do | |
| cargo fmt --manifest-path "native/$crate/Cargo.toml" -- --check | |
| done | |
| - name: Test Rust crates | |
| run: | | |
| for crate in resource-monitor kde-snap-shot hyprland-snap-shot; do | |
| cargo test --locked --manifest-path "native/$crate/Cargo.toml" | |
| done | |
| # The static analysis below needs a macOS runner, which bills ~6.7x a Linux | |
| # minute, so gate it on the native sources it actually lints instead of paying | |
| # for it on every push. Detection is API-only (no checkout) and fails open: if | |
| # the diff cannot be resolved, the lint runs. | |
| mobile_native_changes: | |
| name: Mobile Native Changes | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| changed: ${{ steps.detect.outputs.changed }} | |
| steps: | |
| - name: Detect mobile native changes | |
| id: detect | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| BEFORE_SHA: ${{ github.event.before }} | |
| run: | | |
| set -uo pipefail | |
| fail_open() { | |
| echo "$* Running native static analysis." | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| } | |
| count_rows() { | |
| printf '%s\n' "$1" | grep -c . || true | |
| } | |
| # One row per changed file, holding the new path and, for a rename, | |
| # the path it replaced: renaming a matched file out of the matched | |
| # paths removes a lint input just like editing it. | |
| row='[.filename, (.previous_filename // empty)] | @tsv' | |
| if [[ -n "${PR_NUMBER}" ]]; then | |
| # The PR files endpoint stops at 3000 files and pagination cannot | |
| # extend it, so cross-check against the count the PR itself reports. | |
| expected=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.changed_files') \ | |
| || fail_open "Could not read the pull request." | |
| rows=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate --jq ".[] | ${row}") \ | |
| || fail_open "Could not resolve changed files." | |
| listed=$(count_rows "$rows") | |
| if [[ "$listed" -lt "$expected" ]]; then | |
| fail_open "GitHub listed only ${listed} of ${expected} changed files." | |
| fi | |
| else | |
| rows=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${BEFORE_SHA}...${GITHUB_SHA}" --jq ".files[]? | ${row}") \ | |
| || fail_open "Could not resolve changed files." | |
| # The compare endpoint reports at most 300 files and pagination does | |
| # not extend that list, so a full list may be hiding native changes. | |
| listed=$(count_rows "$rows") | |
| if [[ "$listed" -ge 300 ]]; then | |
| fail_open "GitHub listed ${listed} changed files, the compare endpoint maximum." | |
| fi | |
| fi | |
| paths=$(tr '\t' '\n' <<< "$rows") | |
| # Sources scripts/mobile-native-static-check.ts lints, plus the tool | |
| # and rule configuration that decides how it lints them, plus the | |
| # root package.json that defines the lint:mobile command. | |
| pattern='^apps/mobile/.*\.(swift|kt|kts)$|^apps/mobile/(\.swiftlint\.yml|detekt\.yml|\.editorconfig|Brewfile)$|^scripts/mobile-native-static-check\.ts$|^package\.json$|^\.github/workflows/ci\.yml$' | |
| if grep -qE "$pattern" <<< "$paths"; then | |
| echo "Native sources or lint configuration changed:" | |
| grep -E "$pattern" <<< "$paths" | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "No mobile native sources or lint configuration changed." | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| mobile_native_static_analysis: | |
| name: Mobile Native Static Analysis | |
| needs: mobile_native_changes | |
| # Skip only on an explicit "no": a gate job that failed or errored leaves the | |
| # output empty, and that must run the lint rather than silently skip it. | |
| if: ${{ !cancelled() && needs.mobile_native_changes.outputs.changed != 'false' }} | |
| runs-on: blacksmith-6vcpu-macos-26 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version-file: package.json | |
| cache: true | |
| run-install: | | |
| args: | |
| - --filter=@t3tools/scripts... | |
| - name: Install mobile native static analysis tools | |
| run: brew bundle install --file apps/mobile/Brewfile | |
| - name: Lint mobile native sources | |
| run: vp run lint:mobile | |
| release_smoke: | |
| name: Release Smoke | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - name: Setup Vite+ | |
| uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version-file: package.json | |
| cache: true | |
| run-install: | | |
| args: | |
| - --filter=@t3tools/scripts... | |
| - name: Exercise release-only workflow steps | |
| run: node scripts/release-smoke.ts | |
| # Branch protection requires this job by its name, `Check`. It fails when any | |
| # other job fails, is cancelled, or is skipped without a reason, so splitting | |
| # or resharding jobs needs no ruleset change. Add every new job to `needs`. | |
| # `always()` rather than `!cancelled()`: a skipped job reports success to | |
| # branch protection, so a cancelled run must still produce a failing check. | |
| check: | |
| name: Check | |
| if: ${{ always() }} | |
| needs: | |
| [ | |
| lint, | |
| typecheck, | |
| build, | |
| test, | |
| test_web, | |
| test_server, | |
| rust, | |
| mobile_native_changes, | |
| mobile_native_static_analysis, | |
| release_smoke, | |
| ] | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Require every job to pass | |
| env: | |
| RESULTS: ${{ toJSON(needs) }} | |
| run: | | |
| echo "$RESULTS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"' | |
| # Only the macOS lint skips on purpose, when no native mobile code changed. | |
| echo "$RESULTS" | jq -e 'all(to_entries[]; | |
| .value.result == "success" | |
| or (.key == "mobile_native_static_analysis" and .value.result == "skipped"))' > /dev/null |