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
78 changes: 78 additions & 0 deletions .github/scripts/coverage-comment.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env node
/**
* Post (or update) a coverage summary comment on a pull request.
*
* Usage: node .github/scripts/coverage-comment.mjs <pr-number> [--print]
*
* Reads coverage/coverage-summary.json (json-summary reporter output),
* renders a small markdown table and upserts a comment marked with
* `<!-- coverage-summary -->` so re-runs update instead of stacking.
* `--print` only prints the body (local verification without GitHub writes).
*/

import { spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'

const args = process.argv.slice(2)
const printOnly = args.includes('--print')
const pr = args.find((a) => /^\d+$/.test(a))
if (!pr) {
console.error('usage: coverage-comment.mjs <pr-number> [--print]')
process.exit(1)
}

const summaryPath = 'coverage/coverage-summary.json'
if (!existsSync(summaryPath)) {
console.error(`missing ${summaryPath} - run vitest with --coverage first`)
process.exit(1)
}

const total = JSON.parse(readFileSync(summaryPath, 'utf8')).total
const row = (metric) => {
const m = total[metric]
return `| ${metric[0].toUpperCase() + metric.slice(1)} | ${m.pct.toFixed(2)}% | ${m.covered}/${m.total} |`
}

const body = [
'<!-- coverage-summary -->',
'## Coverage',
'',
'| Metric | % | Covered / Total |',
'| --- | ---: | ---: |',
row('lines'),
row('statements'),
row('functions'),
row('branches'),
'',
].join('\n')

if (printOnly) {
process.stdout.write(body)
process.exit(0)
}

const run = (cmd, ghArgs, options = {}) => {
const res = spawnSync(cmd, ghArgs, { encoding: 'utf8', ...options })
if (res.status !== 0) {
console.error(res.stderr || res.stdout)
process.exit(1)
}
return res.stdout.trim()
}

const repoSlug =
process.env.GITHUB_REPOSITORY ||
run('git', ['remote', 'get-url', 'origin'])
.replace(/^.*github\.com[/:]/, '')
.replace(/\.git$/, '')

const marker = '<!-- coverage-summary -->'
const comments = JSON.parse(run('gh', ['api', `repos/${repoSlug}/issues/${pr}/comments?per_page=100`]))
const existing = comments.find((c) => c.body && c.body.includes(marker))
const endpoint = existing
? `repos/${repoSlug}/issues/comments/${existing.id}`
: `repos/${repoSlug}/issues/${pr}/comments`
run('gh', ['api', endpoint, '-X', existing ? 'PATCH' : 'POST', '--input', '-'], {
input: JSON.stringify({ body }),
})
console.log(existing ? `updated comment ${existing.id}` : `posted coverage comment on #${pr}`)
75 changes: 75 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: CI

on:
push:
branches: [main]
pull_request:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 11

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run oxlint
run: pnpm lint

test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 11

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Playwright chromium
run: pnpm exec playwright install --with-deps chromium

- name: Run tests
run: |
pnpm serve > /tmp/serve.log 2>&1 &
SERVE_PID=$!
trap 'kill $SERVE_PID' EXIT
for i in $(seq 1 60); do
curl -sf -o /dev/null http://localhost:8080/api/oss-config && break
sleep 1
done
curl -sf -o /dev/null http://localhost:8080/api/oss-config || {
echo "test server did not start:"; cat /tmp/serve.log; exit 1
}
pnpm test:ci:coverage

- name: Comment coverage on PR
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: node .github/scripts/coverage-comment.mjs "${{ github.event.pull_request.number }}"
46 changes: 33 additions & 13 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, beforeAll } from 'vitest'
import {
put,
putSymlink,
Expand Down Expand Up @@ -37,11 +37,20 @@ function getObjectName() {
}

describe('integration', () => {
let configured = false

beforeAll(async () => {
const res = await fetch('http://localhost:8080/api/oss-config')
const data = (await res.json()) as OssConfig
configured = !!(data.accessKeyId && data.accessKeySecret && data.bucket && data.region)
})

it('should throw if options are missing', async () => {
await expect(put({} as any, 'obj', new Blob(['x']))).rejects.toThrow(/need accessKeyId/)
})

it('put', async () => {
it('put', async ({ skip }) => {
if (!configured) return skip()
const content = 'put: hello 你好'
const objectName = getObjectName()
const res = await fetch('http://localhost:8080/api/oss-config')
Expand All @@ -61,7 +70,8 @@ describe('integration', () => {
}
})

it('putSymlink', async () => {
it('putSymlink', async ({ skip }) => {
if (!configured) return skip()
const content = 'putSymlink: hello 你好'
const objectName = getObjectName()
const targetObjectName = getObjectName()
Expand All @@ -85,7 +95,8 @@ describe('integration', () => {
}
})

it('signatureUrl', async () => {
it('signatureUrl', async ({ skip }) => {
if (!configured) return skip()
const content = 'signatureUrl: hello 你好'
const objectName = getObjectName()
const res = await fetch('http://localhost:8080/api/oss-config')
Expand All @@ -108,7 +119,8 @@ describe('integration', () => {
}
})

it('signatureUrl with non-ASCII object name', async () => {
it('signatureUrl with non-ASCII object name', async ({ skip }) => {
if (!configured) return skip()
const content = 'signatureUrl 中文: hello 你好'
const objectName = `中文文件-${Date.now()}.txt`
const res = await fetch('http://localhost:8080/api/oss-config')
Expand All @@ -129,7 +141,8 @@ describe('integration', () => {
}
})

it('multipartUpload', async () => {
it('multipartUpload', async ({ skip }) => {
if (!configured) return skip()
const objectName = getObjectName()
const res = await fetch('http://localhost:8080/api/oss-config')
const data = (await res.json()) as OssConfig
Expand Down Expand Up @@ -168,7 +181,8 @@ describe('integration', () => {
}
})

it('put stsToken', async () => {
it('put stsToken', async ({ skip }) => {
if (!configured) return skip()
const content = 'put stsToken: hello 你好'
const objectName = getObjectName()
const res = await fetch('http://localhost:8080/api/sts')
Expand All @@ -194,7 +208,8 @@ describe('integration', () => {
}
})

it('signatureUrl stsToken', async () => {
it('signatureUrl stsToken', async ({ skip }) => {
if (!configured) return skip()
const content = 'signatureUrl: hello 你好'
const objectName = getObjectName()
const res = await fetch('http://localhost:8080/api/sts')
Expand All @@ -220,7 +235,8 @@ describe('integration', () => {
}
})

it('put ArrayBuffer input', async () => {
it('put ArrayBuffer input', async ({ skip }) => {
if (!configured) return skip()
const size = 2 * 1024 * 1024
const bytes = new Uint8Array(size)
for (let i = 0; i < size; i++) bytes[i] = i % 251
Expand All @@ -246,7 +262,8 @@ describe('integration', () => {
}
})

it('multipartUpload ArrayBuffer input', async () => {
it('multipartUpload ArrayBuffer input', async ({ skip }) => {
if (!configured) return skip()
const size = 3 * 1024 * 1024
const bytes = new Uint8Array(size)
for (let i = 0; i < size; i++) bytes[i] = i % 251
Expand Down Expand Up @@ -274,7 +291,8 @@ describe('integration', () => {
}
})

it('multipartUpload resumes from a checkpoint after an interruption', async () => {
it('multipartUpload resumes from a checkpoint after an interruption', async ({ skip }) => {
if (!configured) return skip()
const objectName = getObjectName()
const res = await fetch('http://localhost:8080/api/oss-config')
const data = (await res.json()) as OssConfig
Expand Down Expand Up @@ -333,7 +351,8 @@ describe('integration', () => {
}
})

it('fetch transport put and multipartUpload', async () => {
it('fetch transport put and multipartUpload', async ({ skip }) => {
if (!configured) return skip()
const res = await fetch('http://localhost:8080/api/oss-config')
const data = (await res.json()) as OssConfig
const { accessKeyId, accessKeySecret, region, bucket } = data
Expand Down Expand Up @@ -379,7 +398,8 @@ describe('integration', () => {
}
})

it('bindOptions put', async () => {
it('bindOptions put', async ({ skip }) => {
if (!configured) return skip()
const content = 'bindOptions put: hello 你好'
const objectName = getObjectName()
const res = await fetch('http://localhost:8080/api/oss-config')
Expand Down
7 changes: 3 additions & 4 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ export default defineConfig({
enabled: true,
provider: playwright(),
// https://vitest.dev/config/browser/playwright
instances: [
{ browser: 'chromium' },
],
instances: [{ browser: 'chromium' }],
},
coverage: {
provider: 'v8' // or 'istanbul'
provider: 'v8', // or 'istanbul'
reporter: ['text', 'html', 'json-summary'],
},
},
})
Loading