Skip to content

Commit c2de875

Browse files
committed
ci: add release quality gates to prevent issue #419 class of misses
- Add peer dep range assertion to test suite (catches overly-narrow peerDependencies like "^17" that block React 18/19 consumers at install) - Gate npm publish behind React 17/18/19 matrix validation (publish job requires needs:validate to pass before running) - Add test-react-versions workflow: runs tests against React 17, 18, 19 on every push/PR - Add test-build-artifacts workflow: smoke tests CJS require(), ESM export structure, and type declaration presence after each build - Add size-limit (6 kB budget) + bundle size PR comment workflow via andresz1/size-limit-action - Add test-ts-versions workflow: type-checks against TS 4.5/4.7/4.9/5.0/5.4/latest
1 parent 459674f commit c2de875

8 files changed

Lines changed: 469 additions & 0 deletions

File tree

.github/workflows/publish.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,38 @@ on:
55
types: [published]
66

77
jobs:
8+
# Runs the full test suite against React 17, 18, and 19 before any publish step.
9+
# All three matrix jobs must pass — if any fail, the publish job never runs.
10+
validate:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: true
14+
matrix:
15+
react: ['17', '18', '19']
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Setup Node
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: 20.x
23+
cache: 'yarn'
24+
25+
- name: Install dependencies
26+
run: yarn install --frozen-lockfile
27+
28+
- name: Swap to React ${{ matrix.react }}
29+
run: |
30+
yarn add --dev react@${{ matrix.react }} react-dom@${{ matrix.react }}
31+
32+
- name: Type check
33+
run: yarn ts-check
34+
35+
- name: Unit tests
36+
run: yarn test --runInBand
37+
838
publish:
39+
needs: validate
940
runs-on: ubuntu-latest
1041
permissions:
1142
contents: read

.github/workflows/size.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Bundle Size
2+
3+
on: [pull_request]
4+
5+
jobs:
6+
size:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
11+
- name: Setup Node
12+
uses: actions/setup-node@v4
13+
with:
14+
node-version: 20.x
15+
cache: 'yarn'
16+
17+
- name: Install dependencies
18+
run: yarn install --frozen-lockfile
19+
20+
# Posts a PR comment showing the brotli size diff vs the base branch.
21+
# Fails CI if either artifact exceeds the limit defined in package.json "size-limit".
22+
- uses: andresz1/size-limit-action@v1
23+
with:
24+
github_token: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Test Build Artifacts
2+
3+
on:
4+
push:
5+
branches: [master, 'feat/**', 'fix/**', 'ci/**']
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
format: [cjs, esm]
15+
16+
name: Artifact — ${{ matrix.format }}
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Setup Node
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: 20.x
25+
cache: 'yarn'
26+
27+
- name: Install dependencies
28+
run: yarn install --frozen-lockfile
29+
30+
- name: Build
31+
run: yarn build
32+
33+
- name: Smoke test CJS artifact
34+
if: matrix.format == 'cjs'
35+
run: |
36+
node -e "
37+
const m = require('./dist/index.js');
38+
const component = m.default || m;
39+
if (typeof component !== 'function') {
40+
console.error('CJS default export is not a function, got:', typeof component);
41+
process.exit(1);
42+
}
43+
console.log('CJS OK — default export is a function');
44+
"
45+
46+
- name: Smoke test ESM artifact
47+
if: matrix.format == 'esm'
48+
run: |
49+
# Verify ES module syntax is present (export keyword) and file is non-empty.
50+
# A full dynamic import would require React as an ESM peer which varies by version,
51+
# so we validate structure and let the React-version matrix cover runtime behaviour.
52+
if ! grep -q "^export " dist/index.es.js; then
53+
echo "ESM artifact missing top-level export statement"
54+
exit 1
55+
fi
56+
echo "ESM OK — top-level export found"
57+
node -e "
58+
const fs = require('fs');
59+
const size = fs.statSync('./dist/index.es.js').size;
60+
if (size < 100) { console.error('ESM artifact suspiciously small:', size, 'bytes'); process.exit(1); }
61+
console.log('ESM artifact size OK:', size, 'bytes');
62+
"
63+
64+
- name: Type declarations exist
65+
run: |
66+
if [ ! -f dist/index.d.ts ]; then
67+
echo "dist/index.d.ts is missing"
68+
exit 1
69+
fi
70+
echo "Type declarations OK"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Test React Versions
2+
3+
on:
4+
push:
5+
branches: [master, 'feat/**', 'fix/**', 'ci/**']
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
react: ['17', '18', '19']
15+
16+
name: React ${{ matrix.react }}
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Setup Node
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: 20.x
25+
cache: 'yarn'
26+
27+
- name: Install dependencies
28+
run: yarn install --frozen-lockfile
29+
30+
# Swap the React version before running tests.
31+
# This also catches an overly-narrow peerDependencies range at install time —
32+
# e.g. "^17" blocks React 18/19 and yarn add will fail here first.
33+
- name: Swap to React ${{ matrix.react }}
34+
run: |
35+
yarn add --dev react@${{ matrix.react }} react-dom@${{ matrix.react }}
36+
37+
- name: Type check
38+
run: yarn ts-check
39+
40+
- name: Unit tests
41+
run: yarn test --runInBand
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Test TypeScript Versions
2+
3+
on:
4+
push:
5+
branches: [master, 'feat/**', 'fix/**', 'ci/**']
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
typescript: ['4.5', '4.7', '4.9', '5.0', '5.4', 'latest']
15+
16+
name: TypeScript ${{ matrix.typescript }}
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Setup Node
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: 20.x
25+
cache: 'yarn'
26+
27+
- name: Install dependencies
28+
run: yarn install --frozen-lockfile
29+
30+
- name: Swap to TypeScript ${{ matrix.typescript }}
31+
run: yarn add --dev typescript@${{ matrix.typescript }}
32+
33+
# Run type-check only — tests themselves don't need to pass under every TS version,
34+
# but the public API surface (src/index.tsx + utils) must type-check cleanly.
35+
- name: Type check
36+
run: yarn ts-check

package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"prettify": "prettier --write 'src/**/*'",
2323
"ts-check": "tsc -p tsconfig.json --noEmit",
2424
"test": "jest",
25+
"size": "size-limit",
2526
"prepare": "husky"
2627
},
2728
"repository": {
@@ -51,6 +52,7 @@
5152
"@babel/preset-env": "^7.28.5",
5253
"@babel/preset-react": "^7.28.5",
5354
"@babel/preset-typescript": "^7.28.5",
55+
"@size-limit/preset-small-lib": "^12.0.1",
5456
"@storybook/addon-essentials": "^7.6.0",
5557
"@storybook/react": "^7.6.0",
5658
"@storybook/react-webpack5": "^7.6.0",
@@ -76,13 +78,24 @@
7678
"rollup": "^1.26.3",
7779
"rollup-plugin-node-resolve": "^5.2.0",
7880
"rollup-plugin-typescript2": "^0.25.2",
81+
"size-limit": "^12.0.1",
7982
"storybook": "^7.6.0",
8083
"ts-jest": "^29.4.6",
8184
"typescript": "^4.9.0"
8285
},
8386
"dependencies": {
8487
"throttle-debounce": "^2.1.0"
8588
},
89+
"size-limit": [
90+
{
91+
"path": "dist/index.es.js",
92+
"limit": "6 kB"
93+
},
94+
{
95+
"path": "dist/index.js",
96+
"limit": "6 kB"
97+
}
98+
],
8699
"lint-staged": {
87100
"*.{js,css,json,md}": [
88101
"prettier --write"

src/__tests__/package.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export {};
2+
3+
/**
4+
* Validates package.json fields that affect consumers at install time.
5+
* These checks run on every `yarn test` invocation — no extra infrastructure needed.
6+
*
7+
* Issue class caught: overly-narrow peerDependency ranges (e.g. "^17" instead of ">=17")
8+
* that block React 18/19 consumers at npm install, like #419.
9+
*/
10+
11+
// eslint-disable-next-line @typescript-eslint/no-require-imports
12+
const pkg = require('../../package.json') as {
13+
peerDependencies: Record<string, string>;
14+
devDependencies: Record<string, string>;
15+
};
16+
17+
describe('package.json — peer dependency ranges', () => {
18+
it('react peer dep uses >= (open-ended), not ^ (caret-bounded)', () => {
19+
// "^17.0.0" resolves to >=17 <18 — blocks React 18/19 consumers at install time.
20+
// Must be ">=17.0.0" or similar open range.
21+
expect(pkg.peerDependencies.react).toMatch(/^>=/);
22+
});
23+
24+
it('react-dom peer dep uses >= (open-ended), not ^ (caret-bounded)', () => {
25+
expect(pkg.peerDependencies['react-dom']).toMatch(/^>=/);
26+
});
27+
28+
it('peer dep floor is not higher than the version we test against in devDependencies', () => {
29+
// Guards against bumping the peer dep floor without updating our dev/test version.
30+
// e.g. peerDep ">=19" while devDep is still "^17" would mean our tests don't cover
31+
// the minimum version we claim to support.
32+
const peerFloor = parseInt(
33+
/\d+/.exec(pkg.peerDependencies.react)?.[0] ?? '',
34+
10
35+
);
36+
const devMajor = parseInt(
37+
/\d+/.exec(pkg.devDependencies.react)?.[0] ?? '',
38+
10
39+
);
40+
expect(peerFloor).toBeLessThanOrEqual(devMajor);
41+
});
42+
});

0 commit comments

Comments
 (0)