Skip to content

chore(sync): merge Yarden-zamir/t3code main #65

chore(sync): merge Yarden-zamir/t3code main

chore(sync): merge Yarden-zamir/t3code main #65

Workflow file for this run

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