Repository navigation
Merge pull request #27 from TomCohenDev/fork-sync/2026-11-09-1953 #58
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: Fork CI | |
| # The commands of ci.yml on GitHub-hosted runners. ci.yml uses upstream's | |
| # Blacksmith runners, which this fork does not have, so the fork disables it. | |
| # Limit: this file copies the ci.yml commands by hand. When upstream changes | |
| # them, update this file. A failure on main calls the repair agent. See docs/fork/operations.md "Fork CI". | |
| # | |
| # The changes job runs upstream's .github/scripts/ci-changes.cjs, which maps the | |
| # changed files to the jobs they can affect. It compares a push with the last green | |
| # run, so a cancelled or failed run never hides a change. It compares a pull request | |
| # with its base. | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| notify_agent: | |
| description: Call the agent webhook if this run fails. The agent sets false on its own retries. | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: read | |
| # A newer push to the same branch or pull request cancels the older run. A manual run waits instead. | |
| concurrency: | |
| group: fork-ci-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} | |
| jobs: | |
| changes: | |
| name: Changes | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| actions: read | |
| contents: read | |
| outputs: | |
| code: ${{ steps.classify.outputs.typecheck }} | |
| files: ${{ steps.classify.outputs.files }} | |
| build: ${{ steps.classify.outputs.build }} | |
| test: ${{ steps.classify.outputs.test }} | |
| test_packages: ${{ steps.classify.outputs.test_packages }} | |
| test_web: ${{ steps.classify.outputs.test_web }} | |
| test_server: ${{ steps.classify.outputs.test_server }} | |
| rust: ${{ steps.classify.outputs.rust }} | |
| steps: | |
| # Only the files ci-changes.cjs reads to build the package graph. | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /pnpm-workspace.yaml | |
| /.github/scripts/ci-changes.cjs | |
| /*/package.json | |
| /*/*/package.json | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - name: Classify the changed files | |
| id: classify | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| # Without a list of changed files, ci-changes.cjs runs every job. | |
| everything() { echo "$1: every job runs."; node .github/scripts/ci-changes.cjs | tee -a "$GITHUB_OUTPUT"; exit 0; } | |
| head="$GITHUB_SHA" | |
| case "$GITHUB_EVENT_NAME" in | |
| push) | |
| base="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow fork-ci.yml --branch "$GITHUB_REF_NAME" \ | |
| --event push --status success --limit 1 --json headSha --jq '.[0].headSha // empty')" | |
| [[ -n "$base" ]] || everything "No green run to compare with" | |
| ;; | |
| pull_request) | |
| base="$PR_BASE_SHA" | |
| head="$PR_HEAD_SHA" | |
| ;; | |
| *) everything "Manual run" ;; | |
| esac | |
| # The compare API lists at most 300 files. A longer list runs every job. | |
| # A rename lists the old path too: moving a file out of a package changes that package. | |
| files="$(gh api "repos/$GITHUB_REPOSITORY/compare/$base...$head" \ | |
| --jq '.files[] | .filename, (.previous_filename // empty)')" || | |
| everything "The compare with $base failed" | |
| (($(wc -l <<<"$files") < 300)) || everything "300 or more changed files" | |
| { echo "files<<EOF"; echo "$files"; echo "EOF"; } >>"$GITHUB_OUTPUT" | |
| # Fork: Markdown is docs anywhere. ci-changes.cjs treats only root *.md as docs. | |
| grep -v '\.md$' <<<"$files" >"$RUNNER_TEMP/changed-paths" || true | |
| decision="$(node .github/scripts/ci-changes.cjs --paths "$RUNNER_TEMP/changed-paths")" | |
| # As upstream: a code push to main runs every job, the backstop for a | |
| # dependency the script misses. A docs-only push still skips them. | |
| if [[ "$GITHUB_EVENT_NAME" == push ]] && grep -qx 'typecheck=true' <<<"$decision"; then | |
| decision="$(node .github/scripts/ci-changes.cjs --paths "$RUNNER_TEMP/changed-paths" --full)" | |
| fi | |
| tee -a "$GITHUB_OUTPUT" <<<"$decision" | |
| lint: | |
| name: Lint | |
| needs: changes | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - uses: ./.github/actions/setup-ci-deps | |
| - if: needs.changes.outputs.code == 'true' | |
| run: vp run --filter @t3tools/desktop ensure:electron | |
| - if: needs.changes.outputs.code == 'true' | |
| run: vp run knip:check | |
| - if: needs.changes.outputs.code == 'true' | |
| run: vp check | |
| - name: Check the changed files | |
| if: needs.changes.outputs.code != 'true' | |
| env: | |
| FILES: ${{ needs.changes.outputs.files }} | |
| run: | | |
| existing=() | |
| scripts=() | |
| while IFS= read -r file; do | |
| [[ -f "$file" ]] || continue | |
| existing+=("$file") | |
| case "$file" in *.ts | *.tsx | *.js | *.jsx | *.mjs | *.cjs) scripts+=("$file") ;; esac | |
| done <<<"$FILES" | |
| # vp check fails when no file is lintable, so markdown gets only the format check. | |
| if ((${#existing[@]} > 0)); then vp fmt --check "${existing[@]}"; fi | |
| if ((${#scripts[@]} > 0)); then vp check "${scripts[@]}"; fi | |
| # On both paths, because a code change can rename a file that a doc links to. | |
| - name: Check the links of the fork docs | |
| run: node scripts/fork-check-doc-links.ts | |
| # See docs/fork/operations.md "Adopted upstream pull requests". The check walks the commits | |
| # of HEAD, so it fetches them without their trees and files (a few seconds). It runs before | |
| # the depth-1 upstream fetch below: while upstream main is ahead of HEAD, that fetch leaves | |
| # the clone shallow again. | |
| - name: Check the adopted upstream pull requests | |
| run: | | |
| git fetch --no-tags --filter=tree:0 --unshallow origin "$(git rev-parse HEAD)" | |
| node scripts/fork-adopted-prs.ts check | |
| # See docs/fork/operations.md "Migrations". | |
| # The clone has the commits but not their files, so noop negotiation offers none of them. | |
| # Otherwise upstream sends deltas against files that the clone lacks, and the fetch fails. | |
| - name: Check the migration registry against upstream main | |
| run: | | |
| git -c fetch.negotiationAlgorithm=noop fetch --depth=1 --no-tags https://github.com/pingdotgg/t3code.git main:refs/remotes/upstream/main | |
| node scripts/fork-check-migrations.ts | |
| typecheck: | |
| name: Typecheck | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - uses: ./.github/actions/setup-ci-deps | |
| - run: vp run --filter @t3tools/desktop ensure:electron | |
| - run: vpr typecheck | |
| build: | |
| name: Build | |
| needs: changes | |
| if: needs.changes.outputs.build == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config | |
| - uses: ./.github/actions/setup-ci-deps | |
| - run: vp run --filter @t3tools/desktop ensure:electron | |
| - run: vp run build:desktop | |
| - run: node apps/desktop/scripts/verify-preload-bundle.mjs | |
| test: | |
| name: Test ${{ matrix.group }} | |
| needs: changes | |
| if: needs.changes.outputs.test == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| # Two runners: on one runner, the slowest packages wait for each other. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| group: [apps, packages] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential libsecret-1-dev pkg-config | |
| - uses: ./.github/actions/setup-ci-deps | |
| - run: vp run --filter @t3tools/desktop ensure:electron | |
| - if: matrix.group == 'packages' | |
| run: python3 -B .github/scripts/stage-preview-bundle.test.py | |
| - if: matrix.group == 'packages' | |
| run: node --test .github/scripts/check-nightly-release.test.cjs | |
| # TEST_PACKAGES lists the affected packages, or nothing when all of them are. | |
| # t3 and @t3tools/web have their own jobs. | |
| - name: Test | |
| env: | |
| GROUP: ${{ matrix.group }} | |
| TEST_PACKAGES: ${{ needs.changes.outputs.test_packages }} | |
| run: | | |
| apps=(@t3tools/mobile @t3tools/desktop @t3tools/oxlint-plugin-t3code) | |
| filters=() | |
| if [[ -z "$TEST_PACKAGES" ]]; then | |
| if [[ "$GROUP" == apps ]]; then | |
| for package in "${apps[@]}"; do filters+=(--filter "$package"); done | |
| else | |
| filters=(--filter '!t3' --filter '!@t3tools/monorepo' --filter '!@t3tools/web') | |
| for package in "${apps[@]}"; do filters+=(--filter "!$package"); done | |
| fi | |
| else | |
| for package in $TEST_PACKAGES; do | |
| group=packages | |
| case " ${apps[*]} " in *" $package "*) group=apps ;; esac | |
| if [[ "$group" == "$GROUP" ]]; then filters+=(--filter "$package"); fi | |
| done | |
| fi | |
| if ((${#filters[@]} == 0)); then echo "No affected package in the $GROUP group."; exit 0; fi | |
| vp run --parallel --concurrency-limit 4 "${filters[@]}" test | |
| # On one 4 vCPU runner, the web suite took 3 to 6.5 minutes, most of it in | |
| # module import per test file. ci.yml runs it on one 8 vCPU runner. | |
| test_web: | |
| name: Test Web ${{ matrix.shard }} | |
| needs: changes | |
| if: needs.changes.outputs.test_web == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - uses: ./.github/actions/setup-ci-deps | |
| # The browser project runs React tests in headless Chromium. | |
| - uses: ./.github/actions/fork-setup-chromium | |
| with: | |
| playwright-package: apps/web/node_modules/playwright | |
| - run: vp run --filter @t3tools/web test --shard ${{ matrix.shard }}/${{ strategy.job-total }} | |
| test_server: | |
| name: Test Server ${{ matrix.shard }} | |
| needs: changes | |
| if: needs.changes.outputs.test_server == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4, 5, 6, 7, 8] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - uses: ./.github/actions/setup-ci-deps | |
| - uses: ./.github/actions/fork-setup-chromium | |
| with: | |
| playwright-package: apps/server/node_modules/playwright-core | |
| - run: vp run --filter t3 test --shard ${{ matrix.shard }}/${{ strategy.job-total }} | |
| # kitshn's provider contract scenarios (K-723) against the built server, with the daemon and | |
| # the tests of the kitshn release that pnpm-workspace.yaml pins for @kitshn/local. | |
| kitshn_contract: | |
| name: kitshn contract | |
| needs: changes | |
| if: needs.changes.outputs.test_server == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - uses: ./.github/actions/setup-ci-deps | |
| - run: vp run --filter t3 build:bundle | |
| - run: node scripts/fork-kitshn-contract.ts | |
| rust: | |
| name: Rust | |
| needs: changes | |
| if: needs.changes.outputs.rust == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !/.repos/ | |
| sparse-checkout-cone-mode: false | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt | |
| - name: Check formatting and test | |
| run: | | |
| for crate in resource-monitor kde-snap-shot hyprland-snap-shot; do | |
| cargo fmt --manifest-path "native/$crate/Cargo.toml" -- --check | |
| cargo test --locked --manifest-path "native/$crate/Cargo.toml" | |
| done | |
| # The one check that branch protection requires. A skipped job counts as passed, | |
| # so the docs-only path passes too. A cancelled job counts as failed. | |
| result: | |
| name: Fork CI result | |
| needs: [changes, lint, typecheck, build, test, test_web, test_server, kitshn_contract, rust] | |
| if: always() | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Check the job results | |
| if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') | |
| env: | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| run: | | |
| jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<<"$NEEDS_JSON" | |
| exit 1 | |
| # Starts the repair agent, like Fork Release. Without it, a red | |
| # main goes unnoticed. Off until the T3_AGENT_WEBHOOK_URL secret is set. | |
| # A pull request never calls it: its author sees the failed check. | |
| notify_agent: | |
| name: Notify repair agent | |
| needs: [changes, lint, typecheck, build, test, test_web, test_server, kitshn_contract, rust] | |
| if: failure() && github.event_name != 'pull_request' && github.event.inputs.notify_agent != 'false' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| permissions: | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: scripts/fork-notify-agent.sh | |
| sparse-checkout-cone-mode: false | |
| - name: Call agent webhook | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| WEBHOOK_URL: ${{ secrets.T3_AGENT_WEBHOOK_URL }} | |
| CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }} | |
| CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }} | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| run: scripts/fork-notify-agent.sh |