Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
136 changes: 136 additions & 0 deletions .github/actions/checks-lane/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: effect-e2e checks lane
description: effect-e2e's steps for each phase of systemfsoftware's reusable-checks.yml
inputs:
lane:
description: plan, static, affected, test, test-after, timings, gate or docs
required: true
job:
description: The test job from the plan, as JSON
required: false
default: "{}"
profile:
description: Conformance profile, `local` (pull request) or `per-change` (main and the merge queue)
required: false
default: per-change
runs:
using: composite
steps:
- if: inputs.lane == 'plan' || inputs.lane == 'timings'
uses: systemfsoftware/systemfsoftware/.github/actions/sfs-tools@21dfc32e9927c87a31a37d19e5643445a4e398c3
with:
repository: systemfsoftware/systemfsoftware
ref: 21dfc32e9927c87a31a37d19e5643445a4e398c3
- if: inputs.lane == 'plan'
shell: bash -e {0}
run: corepack enable

- name: Affected lane environment
if: inputs.lane == 'affected'
shell: bash -e {0}
run: printf 'CI=true\nCONFORMANCE_PROFILE=local\nTURBO_LOG_ORDER=stream\n' >> "$GITHUB_ENV"
- uses: systemfsoftware/systemfsoftware/.github/actions/install-deps@21dfc32e9927c87a31a37d19e5643445a4e398c3
if: inputs.lane == 'affected'
- name: Turbo cache
if: inputs.lane == 'affected'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-affected-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-affected-
turbo-${{ runner.os }}-checks-
turbo-${{ runner.os }}-
- name: The tests this change affects or touches
id: affected
if: inputs.lane == 'affected'
shell: bash -e {0}
run: node .github/ci/pr-tests.mjs plan
- name: Cache Playwright browsers
if: inputs.lane == 'affected' && steps.affected.outputs.browsers == 'true'
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
playwright-${{ runner.os }}-
- name: Install Playwright chromium
if: inputs.lane == 'affected' && steps.affected.outputs.browsers == 'true'
shell: bash -e {0}
env:
PACKAGE: ${{ steps.affected.outputs.browser-package }}
run: corepack pnpm --filter "$PACKAGE" exec playwright install --with-deps chromium
- name: Run them
if: inputs.lane == 'affected'
shell: bash -e {0}
env:
BASE: ${{ steps.affected.outputs.base }}
run: node .github/ci/pr-tests.mjs run

- name: Static lane environment
if: inputs.lane == 'static'
shell: bash -e {0}
run: printf 'CI=true\nOXLINT_FORMAT=github\nTSGO_FORMAT=github-actions\nTURBO_LOG_ORDER=stream\n' >> "$GITHUB_ENV"
- uses: systemfsoftware/systemfsoftware/.github/actions/install-deps@21dfc32e9927c87a31a37d19e5643445a4e398c3
if: inputs.lane == 'static'
- uses: cachix/install-nix-action@v31
if: inputs.lane == 'static' || inputs.lane == 'docs'
- name: dprint from the flake
if: inputs.lane == 'static' || inputs.lane == 'docs'
shell: bash -e {0}
run: echo "$(nix build --no-link --print-out-paths .#dprint)/bin" >> "$GITHUB_PATH"
- name: Turbo cache
if: inputs.lane == 'static'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-checks-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-checks-
turbo-${{ runner.os }}-

- name: Test lane environment
if: inputs.lane == 'test'
shell: bash -e {0}
run: printf 'CI=true\n' >> "$GITHUB_ENV"
- name: Local conformance profile
if: inputs.lane == 'test' && inputs.profile == 'local'
shell: bash -e {0}
run: printf 'CONFORMANCE_PROFILE=local\n' >> "$GITHUB_ENV"
- uses: systemfsoftware/systemfsoftware/.github/actions/install-deps@21dfc32e9927c87a31a37d19e5643445a4e398c3
if: inputs.lane == 'test'
- name: Turbo cache
if: inputs.lane == 'test'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-test-${{ fromJSON(inputs.job).id }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-test-${{ fromJSON(inputs.job).id }}-
turbo-${{ runner.os }}-checks-
turbo-${{ runner.os }}-
- name: Cache Playwright browsers
if: inputs.lane == 'test' && fromJSON(inputs.job).browser
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
playwright-${{ runner.os }}-
- name: Install Playwright chromium
if: inputs.lane == 'test' && fromJSON(inputs.job).browser
shell: bash -e {0}
run: corepack pnpm --filter @systemfsoftware/effect-e2e-web exec playwright install --with-deps chromium
- name: Build
if: inputs.lane == 'test'
shell: bash -e {0}
env:
FILTERS: ${{ fromJSON(inputs.job).filters }}
run: corepack pnpm exec turbo run build --output-logs=errors-only --log-order=stream $FILTERS

# Heavy: the browser engine suites run on main and in the merge queue, never on a pull request.
- name: Browser engine suites
if: inputs.lane == 'test-after' && inputs.profile != 'local' && fromJSON(inputs.job).browser && (fromJSON(inputs.job).shard == null || fromJSON(inputs.job).shard.index == 1)
shell: bash -e {0}
env:
FILTERS: ${{ fromJSON(inputs.job).filters }}
run: corepack pnpm exec turbo run test:engine --concurrency=1 --continue --output-logs=new-only --log-order=stream $FILTERS
4 changes: 4 additions & 0 deletions .github/ci/no-tests-before-merge.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { appendFile } from 'node:fs/promises'

const output = process.env.GITHUB_OUTPUT
if (process.argv[2] === 'plan' && output) await appendFile(output, 'jobs=[]\n')
130 changes: 130 additions & 0 deletions .github/ci/pr-tests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { execFile, spawn } from 'node:child_process'
import { appendFile, readFile } from 'node:fs/promises'
import { promisify } from 'node:util'

const exec = promisify(execFile)

const command = async (cmd, args, env = {}) => {
const { stdout } = await exec(cmd, args, { env: { ...process.env, ...env }, maxBuffer: 64 * 1024 * 1024 })
return stdout
}

const git = async (...args) => (await command('git', args)).trim()

const turboLs = async (env, affected) => {
const text = await command('corepack', [
'pnpm',
'exec',
'turbo',
'ls',
...(affected ? ['--affected'] : []),
'--output=json',
], env)
return JSON.parse(text.slice(text.indexOf('{'))).packages.items
}

const manifest = async (pkg) => JSON.parse(await readFile(`${pkg.path}/package.json`, 'utf8'))

const owner = (packages, file) =>
packages
.filter((pkg) => file.startsWith(`${pkg.path}/`))
.reduce((best, pkg) => (best === undefined || pkg.path.length > best.path.length ? pkg : best), undefined)

const otherTestTasks = ['test:engine']

const plan = async (base) => {
const head = await git('rev-parse', 'HEAD')
const scm = { TURBO_SCM_BASE: base, TURBO_SCM_HEAD: head }
const changed = (await git('diff', '--name-only', '--diff-filter=AMR', '-z', base, head)).split('\0').filter(Boolean)
const [all, affected] = await Promise.all([turboLs(scm, false), turboLs(scm, true)])
const manifests = new Map(await Promise.all(all.map(async (pkg) => [pkg.name, await manifest(pkg)])))

const filesByPackage = new Map()
for (const file of changed) {
const pkg = owner(all, file)
if (pkg === undefined) continue
filesByPackage.set(pkg.name, [...(filesByPackage.get(pkg.name) ?? []), file.slice(pkg.path.length + 1)])
}

const commands = [[
'corepack',
'pnpm',
'exec',
'turbo',
'run',
'test',
'--affected',
'--continue',
'--concurrency=50%',
'--output-logs=new-only',
'--log-order=stream',
'--',
`--changed=${base}`,
'--passWithNoTests',
]]
for (const [name, files] of filesByPackage) {
for (const task of otherTestTasks) {
if (manifests.get(name)?.scripts?.[task] === undefined) continue
commands.push([
'corepack',
'pnpm',
'exec',
'turbo',
'run',
task,
`--filter=${name}`,
'--output-logs=new-only',
'--',
'--passWithNoTests',
...files,
])
}
}

const browserPackages = affected.filter((pkg) => {
const json = manifests.get(pkg.name)
return json?.scripts?.test !== undefined && json.devDependencies?.playwright !== undefined
})
return { head, scm, changed, affected, commands, browserPackages }
}

const status = (cmd, args, env) =>
new Promise((resolve) => {
const child = spawn(cmd, args, { env: { ...process.env, ...env }, stdio: 'inherit' })
child.on('close', (code) => resolve(code ?? 1))
child.on('error', () => resolve(1))
})

const mode = process.argv[2]
if (mode !== 'plan' && mode !== 'run') {
console.error('usage: pr-tests.mjs plan|run')
process.exit(2)
}
const base = process.env.BASE || (await git('rev-parse', 'HEAD^1'))
const { head, scm, changed, affected, commands, browserPackages } = await plan(base)

if (mode === 'plan') {
console.log(`base ${base}, head ${head}: ${changed.length} changed files, ${affected.length} affected packages`)
for (const pkg of affected) console.log(` affected ${pkg.name}`)
for (const cmd of commands) console.log(cmd.join(' '))
const output = process.env.GITHUB_OUTPUT
if (output) {
const browserPackage = browserPackages[0]?.name ?? ''
await appendFile(
output,
`base=${base}\nbrowsers=${browserPackages.length > 0}\nbrowser-package=${browserPackage}\n`,
)
}
} else {
let failed = false
for (const [cmd, ...args] of commands) {
console.log(`::group::${[cmd, ...args].join(' ')}`)
const code = await status(cmd, args, scm)
console.log('::endgroup::')
if (code !== 0) {
console.log(`::error title=pr-tests::${[cmd, ...args].join(' ')} exited ${code}`)
failed = true
}
}
process.exit(failed ? 1 : 0)
}
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

# Fast checks before the merge, heavy suites after it (owner, 2026-10-29: "They need to be fast with the heavier
# checks on main"), the shape of core's #881. A pull request and a merge-queue draft run the static lane and the
# `affected` lane (.github/ci/pr-tests.mjs), and their plan schedules no packed test job
# (.github/ci/no-tests-before-merge.mjs). Main runs every package's whole suite in the packed test jobs.
on:
push:
branches: [main]
pull_request:
branches: [main]
merge_group:
workflow_dispatch:
# Every commit on main gets its own run and verdict; on any other ref a newer push cancels the superseded run.
concurrency:
group: ci-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

permissions:
contents: read

jobs:
conflicts:
name: no merge conflicts
uses: systemfsoftware/systemfsoftware/.github/workflows/conflict-check.yml@21dfc32e9927c87a31a37d19e5643445a4e398c3
with:
base_ref: ${{ github.event.pull_request.base.ref }}
head_sha: ${{ github.event.pull_request.head.sha }}
checks:
name: build · lint · typecheck · test
needs: conflicts
permissions:
contents: read
actions: read
uses: systemfsoftware/systemfsoftware/.github/workflows/reusable-checks.yml@21dfc32e9927c87a31a37d19e5643445a4e398c3
with:
profile: ${{ github.ref == 'refs/heads/main' && 'per-change' || 'local' }}
record: test-timings-${{ github.ref == 'refs/heads/main' && 'per-change' || 'local' }}
timings: ${{ github.ref == 'refs/heads/main' && 'test-timings' || 'node .github/ci/no-tests-before-merge.mjs' }}
node-options: --max-old-space-size=4096
pnpm: corepack pnpm
test-flags: --output-logs=new-only --log-order=stream --log-prefix=none
gate-name: the gate (pnpm check:ci)
lanes: >-
${{ github.ref == 'refs/heads/main'
&& '[{"id": "static", "name": "static checks (pnpm check:static)", "timeout-minutes": 45, "node-options": "--max-old-space-size=4096", "run": "corepack pnpm check:static", "docs-run": "./bin/dprint check"}]'
|| '[{"id": "static", "name": "static checks (pnpm check:static)", "timeout-minutes": 45, "node-options": "--max-old-space-size=4096", "run": "corepack pnpm check:static", "docs-run": "./bin/dprint check"}, {"id": "affected", "name": "affected tests (turbo --affected, vitest --changed)", "timeout-minutes": 30, "fetch-depth": 2, "node-options": "--max-old-space-size=4096"}]' }}
13 changes: 13 additions & 0 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Commitlint

on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
commitlint:
name: commitlint
uses: systemfsoftware/systemfsoftware/.github/workflows/reusable-commitlint.yml@21dfc32e9927c87a31a37d19e5643445a4e398c3
21 changes: 21 additions & 0 deletions .github/workflows/nix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Nix

# Heavy: builds every flake output that changed. Runs on main and on demand, never on a pull request.
on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
nix:
name: nix
uses: systemfsoftware/systemfsoftware/.github/workflows/reusable-nix.yml@21dfc32e9927c87a31a37d19e5643445a4e398c3
with:
systems: '[{"system": "x86_64-linux", "runner": "ubuntu-latest"}]'
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
node_modules/
dist/
.turbo/
coverage/
.vitest/
reports/
__tmp__/
.stryker-tmp/
temp/
*.tsbuildinfo
.DS_Store
.npmrc
.direnv/
/result
/result-*
/deno.lock
.timings/
.sfs-tools/
# Core tarballs, built by `nix build .#sfs-deps` (CI: the sfs-deps job); never committed
.sfs-deps/
13 changes: 13 additions & 0 deletions .mergify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Mergify: the policy lives in systemfsoftware/.github (.mergify.yml). This file only names this repository's checks.
extends: .github

queue_rules:
- name: default
queue_conditions:
- check-success = no merge conflicts
- check-success = build · lint · typecheck · test / static checks (pnpm check:static)
- or:
- check-success = commitlint / lint PR commits
- check-skipped = commitlint / lint PR commits
merge_conditions:
- check-success = build · lint · typecheck · test / the gate (pnpm check:ci)
Loading
Loading