Skip to content

Commit 5ace78d

Browse files
committed
ci: add ecosystem test against vitejs/devtools
Pack the local devframe build, clone vitejs/devtools at its latest released tag, override its devframe dependency via pnpm.overrides, and run the downstream install/build/test to catch regressions before release. Triggered manually (with an optional ref input) or on a weekly schedule.
1 parent 1efe914 commit 5ace78d

4 files changed

Lines changed: 164 additions & 0 deletions

File tree

.github/workflows/ecosystem-ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Ecosystem CI
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
ref:
7+
description: 'vitejs/devtools ref to test against (tag, branch, or commit). Defaults to latest released tag.'
8+
required: false
9+
type: string
10+
schedule:
11+
- cron: '0 4 * * 1'
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
vitejs-devtools:
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 30
20+
steps:
21+
- uses: actions/checkout@v4
22+
- uses: pnpm/action-setup@v4
23+
- uses: actions/setup-node@v4
24+
with:
25+
node-version: 22
26+
cache: pnpm
27+
- run: pnpm install --frozen-lockfile
28+
- run: pnpm test:ecosystem
29+
env:
30+
ECOSYSTEM_DEVTOOLS_REF: ${{ inputs.ref }}
31+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32+
- if: failure()
33+
uses: actions/upload-artifact@v4
34+
with:
35+
name: ecosystem-devtools-logs
36+
path: |
37+
.ecosystem/devtools/**/*.log
38+
.ecosystem/devtools/packages/**/test-results/**
39+
retention-days: 7

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ test-results
1919
playwright-report
2020
playwright/.cache
2121
blob-report
22+
.ecosystem

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"test": "turbo run build && vitest",
2424
"test:e2e": "turbo run build && playwright test",
2525
"test:e2e:ui": "turbo run build && playwright test --ui",
26+
"test:ecosystem": "tsx scripts/ecosystem-ci.ts",
2627
"release": "bumpp -r",
2728
"typecheck": "tsc -b",
2829
"postinstall": "npx simple-git-hooks && skills-npm"

scripts/ecosystem-ci.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { spawnSync } from 'node:child_process'
2+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3+
import { dirname, resolve } from 'node:path'
4+
import process from 'node:process'
5+
import { fileURLToPath } from 'node:url'
6+
7+
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
8+
const ecosystemDir = resolve(rootDir, '.ecosystem')
9+
const devtoolsDir = resolve(ecosystemDir, 'devtools')
10+
11+
const REPO_URL = 'https://github.com/vitejs/devtools.git'
12+
const RELEASES_API = 'https://api.github.com/repos/vitejs/devtools/releases/latest'
13+
const KEEP = process.env.ECOSYSTEM_KEEP === '1'
14+
15+
interface PackageManifest {
16+
name: string
17+
version: string
18+
pnpm?: {
19+
overrides?: Record<string, string>
20+
[key: string]: unknown
21+
}
22+
[key: string]: unknown
23+
}
24+
25+
async function main(): Promise<void> {
26+
const ref = await resolveRef()
27+
log(`Target: vitejs/devtools @ ${ref}`)
28+
29+
const tarball = packDevframe()
30+
log(`Packed devframe: ${tarball}`)
31+
32+
prepareClone(ref)
33+
patchPackageJson(devtoolsDir, tarball)
34+
35+
run('pnpm', ['install', '--no-frozen-lockfile'], devtoolsDir)
36+
run('pnpm', ['build'], devtoolsDir)
37+
run('pnpm', ['test'], devtoolsDir)
38+
39+
log('All downstream checks passed')
40+
}
41+
42+
async function resolveRef(): Promise<string> {
43+
const override = process.env.ECOSYSTEM_DEVTOOLS_REF
44+
if (override)
45+
return override
46+
47+
const headers: Record<string, string> = { 'user-agent': 'devframe-ecosystem-ci' }
48+
if (process.env.GITHUB_TOKEN)
49+
headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`
50+
51+
const res = await fetch(RELEASES_API, { headers })
52+
if (!res.ok)
53+
throw new Error(`Failed to query latest release: ${res.status} ${res.statusText}`)
54+
const data = await res.json() as { tag_name?: string }
55+
if (!data.tag_name)
56+
throw new Error('GitHub release payload missing tag_name')
57+
return data.tag_name
58+
}
59+
60+
function packDevframe(): string {
61+
const devframePkg = resolve(rootDir, 'packages', 'devframe')
62+
const pkg = readManifest(resolve(devframePkg, 'package.json'))
63+
const expected = `${pkg.name}-${pkg.version}.tgz`
64+
65+
mkdirSync(ecosystemDir, { recursive: true })
66+
for (const f of readdirSync(ecosystemDir)) {
67+
if (/^devframe-\d.*\.tgz$/.test(f))
68+
rmSync(resolve(ecosystemDir, f))
69+
}
70+
71+
run('pnpm', ['pack', '--pack-destination', ecosystemDir], devframePkg)
72+
73+
const tarball = resolve(ecosystemDir, expected)
74+
if (!existsSync(tarball))
75+
throw new Error(`Expected packed tarball not found: ${tarball}`)
76+
return tarball
77+
}
78+
79+
function prepareClone(ref: string): void {
80+
mkdirSync(ecosystemDir, { recursive: true })
81+
82+
if (KEEP && existsSync(devtoolsDir)) {
83+
log(`Reusing existing clone at ${devtoolsDir} (ECOSYSTEM_KEEP=1)`)
84+
return
85+
}
86+
87+
if (existsSync(devtoolsDir))
88+
rmSync(devtoolsDir, { recursive: true, force: true })
89+
90+
run('git', ['clone', '--depth', '1', '--branch', ref, REPO_URL, devtoolsDir])
91+
}
92+
93+
function patchPackageJson(repoDir: string, tarball: string): void {
94+
const pkgPath = resolve(repoDir, 'package.json')
95+
const pkg = readManifest(pkgPath)
96+
pkg.pnpm ??= {}
97+
pkg.pnpm.overrides = { ...(pkg.pnpm.overrides ?? {}), devframe: `file:${tarball}` }
98+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
99+
log(`Patched ${pkgPath}: devframe -> file:${tarball}`)
100+
}
101+
102+
function readManifest(file: string): PackageManifest {
103+
return JSON.parse(readFileSync(file, 'utf8')) as PackageManifest
104+
}
105+
106+
function run(cmd: string, args: string[], cwd: string = rootDir): void {
107+
log(`$ ${cmd} ${args.join(' ')} (in ${cwd})`)
108+
const result = spawnSync(cmd, args, { cwd, stdio: 'inherit', shell: false })
109+
if (result.status !== 0) {
110+
throw new Error(
111+
`Command failed (exit ${result.status ?? 'unknown'}): ${cmd} ${args.join(' ')}`,
112+
)
113+
}
114+
}
115+
116+
function log(msg: string): void {
117+
console.log(`[ecosystem-ci] ${msg}`)
118+
}
119+
120+
main().catch((err) => {
121+
console.error(err instanceof Error ? err.message : err)
122+
process.exit(1)
123+
})

0 commit comments

Comments
 (0)