Repository navigation
376 lines (361 loc) · 14.7 KB
/
Copy pathfork-ci.yml
File metadata and controls
376 lines (361 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
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]
# A job GitHub never gave a runner, or one past timeout-minutes, ends 'cancelled' and the
# run 'failure', which failure() misses. !cancelled() still skips runs a person cancelled.
if: ${{ !cancelled() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && 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