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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ecosystem-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Ecosystem CI

on:
workflow_dispatch:
inputs:
ref:
description: 'vitejs/devtools ref to test against (tag, branch, or commit). Defaults to latest released tag.'
required: false
type: string
schedule:
- cron: '0 4 * * 1'

permissions:
contents: read

jobs:
vitejs-devtools:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test:ecosystem
env:
ECOSYSTEM_DEVTOOLS_REF: ${{ inputs.ref }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: failure()
uses: actions/upload-artifact@v4
with:
name: ecosystem-devtools-logs
path: |
.ecosystem/devtools/**/*.log
.ecosystem/devtools/packages/**/test-results/**
retention-days: 7
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ test-results
playwright-report
playwright/.cache
blob-report
.ecosystem
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"test": "turbo run build && vitest",
"test:e2e": "turbo run build && playwright test",
"test:e2e:ui": "turbo run build && playwright test --ui",
"test:ecosystem": "tsx scripts/ecosystem-ci.ts",
"release": "bumpp -r",
"typecheck": "tsc -b",
"postinstall": "npx simple-git-hooks && skills-npm"
Expand Down
130 changes: 130 additions & 0 deletions scripts/ecosystem-ci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { spawnSync } from 'node:child_process'
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'

const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const ecosystemDir = resolve(rootDir, '.ecosystem')
const devtoolsDir = resolve(ecosystemDir, 'devtools')

const REPO_URL = 'https://github.com/vitejs/devtools.git'
const RELEASES_API = 'https://api.github.com/repos/vitejs/devtools/releases/latest'
const KEEP = process.env.ECOSYSTEM_KEEP === '1'

interface PackageManifest {
name: string
version: string
pnpm?: {
overrides?: Record<string, string>
[key: string]: unknown
}
[key: string]: unknown
}

async function main(): Promise<void> {
const ref = await resolveRef()
log(`Target: vitejs/devtools @ ${ref}`)

const tarball = packDevframe()
log(`Packed devframe: ${tarball}`)

prepareClone(ref)
patchPackageJson(devtoolsDir, tarball)

run('pnpm', ['install', '--no-frozen-lockfile'], devtoolsDir)
run('pnpm', ['build'], devtoolsDir)
run('pnpm', ['test'], devtoolsDir)

log('All downstream checks passed')
}

async function resolveRef(): Promise<string> {
const override = process.env.ECOSYSTEM_DEVTOOLS_REF
if (override)
return override

const headers: Record<string, string> = { 'user-agent': 'devframe-ecosystem-ci' }
if (process.env.GITHUB_TOKEN)
headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`

const res = await fetch(RELEASES_API, { headers })
if (!res.ok)
throw new Error(`Failed to query latest release: ${res.status} ${res.statusText}`)
const data = await res.json() as { tag_name?: string }
if (!data.tag_name)
throw new Error('GitHub release payload missing tag_name')
return data.tag_name
}

function packDevframe(): string {
const devframePkg = resolve(rootDir, 'packages', 'devframe')
const pkg = readManifest(resolve(devframePkg, 'package.json'))
const expected = `${pkg.name}-${pkg.version}.tgz`

mkdirSync(ecosystemDir, { recursive: true })
for (const f of readdirSync(ecosystemDir)) {
if (/^devframe-\d.*\.tgz$/.test(f))
rmSync(resolve(ecosystemDir, f))
}

run('pnpm', ['pack', '--pack-destination', ecosystemDir], devframePkg)

const tarball = resolve(ecosystemDir, expected)
if (!existsSync(tarball))
throw new Error(`Expected packed tarball not found: ${tarball}`)
return tarball
}

function prepareClone(ref: string): void {
mkdirSync(ecosystemDir, { recursive: true })

if (KEEP && existsSync(devtoolsDir)) {
log(`Reusing existing clone at ${devtoolsDir} (ECOSYSTEM_KEEP=1)`)
return
}

if (existsSync(devtoolsDir))
rmSync(devtoolsDir, { recursive: true, force: true })

// Use init + fetch instead of `clone --branch` so any ref works — tag,
// branch, or commit SHA. GitHub allows fetching reachable SHAs by default.
mkdirSync(devtoolsDir, { recursive: true })
run('git', ['init', '--quiet'], devtoolsDir)
run('git', ['remote', 'add', 'origin', REPO_URL], devtoolsDir)
run('git', ['fetch', '--depth', '1', 'origin', ref], devtoolsDir)
run('git', ['checkout', '--quiet', 'FETCH_HEAD'], devtoolsDir)
}

function patchPackageJson(repoDir: string, tarball: string): void {
const pkgPath = resolve(repoDir, 'package.json')
const pkg = readManifest(pkgPath)
pkg.pnpm ??= {}
pkg.pnpm.overrides = { ...(pkg.pnpm.overrides ?? {}), devframe: `file:${tarball}` }
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
log(`Patched ${pkgPath}: devframe -> file:${tarball}`)
}

function readManifest(file: string): PackageManifest {
return JSON.parse(readFileSync(file, 'utf8')) as PackageManifest
}

function run(cmd: string, args: string[], cwd: string = rootDir): void {
log(`$ ${cmd} ${args.join(' ')} (in ${cwd})`)
const result = spawnSync(cmd, args, { cwd, stdio: 'inherit', shell: false })
if (result.error)
throw new Error(`Command failed to spawn: ${cmd} ${args.join(' ')}: ${result.error.message}`)
if (result.signal)
throw new Error(`Command terminated by signal ${result.signal}: ${cmd} ${args.join(' ')}`)
if (result.status !== 0)
throw new Error(`Command failed (exit ${result.status ?? 'unknown'}): ${cmd} ${args.join(' ')}`)
}

function log(msg: string): void {
console.log(`[ecosystem-ci] ${msg}`)
}

main().catch((err) => {
console.error(err instanceof Error ? err.message : err)
process.exit(1)
})
Loading