project-biosciences-qms-npm-publishing-workflow
title: NPM Package Publishing Workflow - Automated CI/CD for @coditect/* Packages type: reference component_type: reference version: 1.0.0 created: 2026-02-16 updated: 2026-02-16 status: active track: A tags:
- npm
- publishing
- ci-cd
- semantic-versioning
- github-actions
- automation
- package-management
- @coditect/doc-viewer
- @coditect/create-doc-site summary: | Comprehensive technical guide for automated npm package publishing with semantic versioning, CI/CD automation via GitHub Actions, quality gates, and multi-package coordination for @coditect/doc-viewer and @coditect/create-doc-site.
NPM Package Publishing Workflow
Evidence Document for Automated Publishing Infrastructure
Table of Contents
- Overview
- Registry Strategy
- Semantic Versioning Policy
- CI/CD Pipeline Architecture
- Automated Changelog Generation
- Quality Gates
- Publish Process
- Rollback Strategy
- Monitoring and Analytics
- Multi-Package Coordination
- Configuration Files
- Security and Access Control
- Troubleshooting
Overview
Packages in Scope
This document covers the automated publishing workflow for two npm packages:
| Package | Purpose | Type | Dependencies |
|---|---|---|---|
@coditect/doc-viewer | React component library for documentation sites | Library | React, TypeScript, MDX |
@coditect/create-doc-site | CLI scaffolding tool for documentation sites | CLI | Node.js, Templates |
Automation Goals
- Zero-Touch Publishing: Commit → CI → Test → Version → Publish → Release Notes
- Quality Enforcement: No bad versions reach npm registry
- Reproducibility: Every publish includes provenance and audit trail
- Fast Feedback: Developers know immediately if changes break publishing
- Safe Rollback: Quick recovery from bad publishes
Architecture Overview
Developer Commits → GitHub PR → CI Checks → Merge to Main
↓
Semantic Version
↓
GitHub Release
↓
NPM Publish
↓
Update Docs Site
Registry Strategy
NPM Public vs GitHub Packages
Decision: Use NPM Public Registry
| Factor | NPM Public | GitHub Packages |
|---|---|---|
| Discovery | Excellent (npmjs.com) | Limited (GitHub users only) |
| CDN | Global, fast | GitHub CDN |
| Pricing | Free for public | Free for public |
| Installation | Zero config | Requires .npmrc auth |
| Provenance | Supported (npm 9+) | Supported |
| Analytics | npm downloads API | GitHub stats |
Why NPM Public:
- Better discoverability for open-source packages
- Zero friction for consumers (no auth required)
- Industry standard for React component libraries
- Built-in provenance support
Use GitHub Packages for:
- Internal/private packages
- Pre-release testing builds
- Canary releases before npm publish
Scoped Package: @coditect
Organization Setup:
# 1. Create npm organization (one-time)
npm login
npm org create coditect
# 2. Configure scope access
npm access public @coditect/doc-viewer
npm access public @coditect/create-doc-site
# 3. Add team members
npm org set coditect:developers developer1
npm org set coditect:developers developer2
Benefits of Scoped Packages:
- Namespace ownership (no collision with unscoped packages)
- Professional branding (@coditect/*)
- Easier to discover related packages
- Can have private and public packages under same scope
Access Control
Team Structure:
Organization: coditect
├── Owners (admin access)
│ └── halcasteel
├── Developers (publish access)
│ ├── developer1
│ ├── developer2
│ └── ci-bot (automation token)
└── Consumers (read-only)
└── Public (everyone)
npm Tokens:
| Token Type | Purpose | Scope | Expiry |
|---|---|---|---|
| Automation Token | CI/CD publish | Write to @coditect/* | 1 year |
| Developer Token | Manual publish | Write to @coditect/* | 90 days |
| Read Token | Testing private | Read @coditect/* | Never |
Creating Automation Token:
# 1. Create automation token (no 2FA required)
npm token create --read-write --cidr=0.0.0.0/0
# 2. Store in GitHub Secrets
gh secret set NPM_TOKEN --body "npm_xxxxxxxxx"
# 3. Rotate annually
npm token revoke <token-id>
npm token create --read-write --cidr=0.0.0.0/0
.npmrc Configuration
Project .npmrc (committed to git):
# Registry configuration
registry=https://registry.npmjs.org/
@coditect:registry=https://registry.npmjs.org/
# Scope access
access=public
# Package lock configuration
package-lock=true
save-exact=true
# Security settings
audit=true
audit-level=high
# Publishing settings
sign-git-tag=true
git-tag-version=true
User .npmrc (developer local, NOT committed):
# Authentication (never commit this)
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
# Email for package publishing
email=engineering@coditect.ai
# Two-factor authentication
otp=true
CI/CD .npmrc (generated at runtime):
# Dynamically created in GitHub Actions
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
registry=https://registry.npmjs.org/
always-auth=true
GitHub Packages Fallback
Use case: Pre-release testing before npm publish
# .github/.npmrc-github
registry=https://npm.pkg.github.com
@coditect:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
Publish to GitHub Packages:
# Override registry for testing
npm publish --registry https://npm.pkg.github.com
# Consumers install with auth
npm install @coditect/doc-viewer@1.0.0-canary.1 --registry https://npm.pkg.github.com
Semantic Versioning Policy
SemVer Rules for @coditect/doc-viewer
Component Library Versioning:
| Version Change | Trigger | Examples |
|---|---|---|
| MAJOR (x.0.0) | Breaking API changes | Prop renamed, component removed, peer dep upgrade |
| MINOR (0.x.0) | New features (backward compatible) | New component, new prop, new export |
| PATCH (0.0.x) | Bug fixes, docs, internal refactors | Fix rendering bug, update docs, refactor internals |
Detailed Breaking Change Policy:
MAJOR Version Required:
- Removing a component:
<DocumentViewer>→ removed - Renaming a prop:
theme→colorScheme - Changing prop type:
theme: string→theme: ThemeConfig - Removing a prop (even with deprecation warning for 1+ minor versions)
- Peer dependency major version bump:
react@17→react@18 - CSS class name changes (if consumers rely on them)
- Bundle format changes: ESM-only → ESM+CJS
- Minimum Node.js version increase: Node 16+ → Node 18+
MINOR Version Allowed:
- Adding a new component:
<SearchBar>added - Adding a new optional prop:
<DocumentViewer enablePrint?> - Adding a new export:
export { useDocumentContext } - Deprecating (but not removing) a prop
- Peer dependency minor/patch bump (if compatible)
- Internal dependency update (if no API change)
PATCH Version Allowed:
- Fixing a rendering bug
- Performance optimization (no API change)
- Documentation updates
- TypeScript type narrowing (more specific, not breaking)
- Dependency patch updates
- Internal refactoring
SemVer Rules for @coditect/create-doc-site
CLI Tool Versioning:
| Version Change | Trigger | Examples |
|---|---|---|
| MAJOR (x.0.0) | Breaking CLI changes | Flag removed, default behavior change, output structure change |
| MINOR (0.x.0) | New features | New template, new flag, new command |
| PATCH (0.0.x) | Bug fixes, template updates | Fix scaffold bug, update dependency versions in template |
MAJOR Version Required:
- Removing a CLI flag:
--typescript→ removed - Changing default behavior: default to TypeScript instead of JavaScript
- Changing scaffolded project structure
- Removing a template:
--template minimal→ removed - Node.js version requirement increase
MINOR Version Allowed:
- Adding a new template:
--template documentation-pro - Adding a new flag:
--package-manager pnpm - Adding a new command:
create-doc-site upgrade
PATCH Version Allowed:
- Fixing scaffolding bug
- Updating template dependencies:
react@18.2.0→react@18.3.0 - Documentation improvements
Pre-Release Versions
Pre-release Tags:
| Tag | Purpose | Stability | Audience |
|---|---|---|---|
| alpha | Early development | Unstable | Internal testing |
| beta | Feature complete | Mostly stable | Early adopters |
| rc | Release candidate | Stable | Final validation |
| canary | Feature branch | Varies | Specific testing |
Version Format:
1.2.3-alpha.1 # Alpha release 1 of version 1.2.3
1.2.3-beta.2 # Beta release 2
1.2.3-rc.1 # Release candidate 1
1.2.3-canary.4 # Canary build 4 (from feature branch)
SemVer Precedence:
1.0.0-alpha.1 < 1.0.0-alpha.2 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0
Publishing Pre-releases:
# Alpha (internal testing)
npm version prerelease --preid=alpha
npm publish --tag alpha
# Beta (early adopters)
npm version prerelease --preid=beta
npm publish --tag beta
# Release candidate (final validation)
npm version prerelease --preid=rc
npm publish --tag rc
# Canary (feature branch)
npm version prerelease --preid=canary
npm publish --tag canary
Consumer Installation:
# Latest stable
npm install @coditect/doc-viewer
# Latest beta
npm install @coditect/doc-viewer@beta
# Specific pre-release
npm install @coditect/doc-viewer@1.2.3-beta.2
Version Range Compatibility
Recommended Dependency Ranges:
| Dependency Type | Range | Rationale |
|---|---|---|
| peerDependencies | ^18.0.0 | Allow minor/patch updates |
| dependencies | ^1.2.3 | Allow minor/patch updates |
| devDependencies | ^1.2.3 | Allow minor/patch updates |
| engines | >=18.0.0 | Minimum version only |
peerDependencies for @coditect/doc-viewer:
{
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": false
},
"react-dom": {
"optional": false
}
}
}
Why ^18.0.0 instead of >=18.0.0:
^18.0.0allows18.x.x(minor + patch), blocks19.0.0(major)- Prevents consumers from using untested React 19+ with our library
- We can support React 19 in next major version after testing
Consumer Impact:
# Consumer with React 18.2.0 - works
npm install @coditect/doc-viewer
# Consumer with React 19.0.0 - peer dep conflict
npm install @coditect/doc-viewer
# Error: peer react@^18.0.0 from @coditect/doc-viewer@1.0.0
# Consumer can override (at their own risk)
npm install @coditect/doc-viewer --legacy-peer-deps
Version Alignment Strategy
Multi-Package Versioning:
| Strategy | Approach | Pros | Cons |
|---|---|---|---|
| Independent | Each package has own version | Semantic clarity | Confusion about compatibility |
| Fixed | All packages share same version | Simplicity | Unnecessary major bumps |
| Hybrid | Major aligned, minor independent | Balance | Requires coordination |
Decision: Hybrid Versioning
@coditect/doc-viewer@2.5.3
@coditect/create-doc-site@2.1.0
Rules:
- Major versions always aligned (both on v2.x.x)
- Minor/patch versions independent
create-doc-sitescaffolds compatibledoc-viewerversion
Compatibility Matrix:
| create-doc-site | Compatible doc-viewer | Rationale |
|---|---|---|
| 2.1.x | ^2.0.0 | Scaffolds any v2 |
| 2.2.x | ^2.0.0 | Scaffolds any v2 |
| 3.0.x | ^3.0.0 | Scaffolds any v3 |
CI/CD Pipeline Architecture
Pipeline Overview
Three Workflows:
- PR Checks (
.github/workflows/pr-checks.yml) - Quality gates on every PR - Release (
.github/workflows/release.yml) - Publish on version tag push - Canary (
.github/workflows/canary.yml) - Publish pre-release from feature branches
Workflow 1: PR Checks
Triggers:
- Pull request opened/updated
- Push to any branch (except main)
Jobs:
- Lint (ESLint, Prettier)
- Type check (TypeScript)
- Unit tests (Vitest)
- Integration tests (Playwright)
- Build (TypeScript → JavaScript)
- Bundle size check (size-limit)
- Security audit (npm audit)
Complete YAML:
name: PR Checks
on:
pull_request:
branches: [main, develop]
push:
branches-ignore: [main]
concurrency:
group: pr-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Check Prettier formatting
run: npm run format:check
typecheck:
name: TypeScript
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Build declaration files
run: npm run build:types
test:
name: Test (Node ${{ matrix.node }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
node: [18, 20, 22]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json
flags: unit-${{ matrix.node }}
test-integration:
name: Integration Tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Run integration tests
run: npm run test:integration
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: test-results/
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build package
run: npm run build
- name: Check exports
run: node scripts/check-exports.js
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
bundle-size:
name: Bundle Size
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check bundle size
run: npm run size
- name: Comment PR with bundle size
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
limit: 50KB
security:
name: Security Audit
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Run npm audit
run: npm audit --audit-level=high
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
pr-status:
name: PR Status
runs-on: ubuntu-latest
needs: [lint, typecheck, test, test-integration, build, bundle-size, security]
if: always()
steps:
- name: Check all jobs passed
run: |
if [[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.typecheck.result }}" != "success" ]] || \
[[ "${{ needs.test.result }}" != "success" ]] || \
[[ "${{ needs.test-integration.result }}" != "success" ]] || \
[[ "${{ needs.build.result }}" != "success" ]] || \
[[ "${{ needs.bundle-size.result }}" != "success" ]] || \
[[ "${{ needs.security.result }}" != "success" ]]; then
echo "One or more checks failed"
exit 1
fi
Workflow 2: Release
Triggers:
- Tag push matching
v*.*.*pattern (e.g.,v1.2.3)
Jobs:
- Run all PR checks
- Build package
- Publish to npm with provenance
- Create GitHub release
- Generate changelog
- Update documentation site
Complete YAML:
name: Release
on:
push:
tags:
- 'v*.*.*'
permissions:
contents: write
packages: write
id-token: write # Required for npm provenance
jobs:
validate:
name: Validate Release
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.version.outputs.version }}
is-prerelease: ${{ steps.version.outputs.is-prerelease }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version from tag
id: version
run: |
TAG=${GITHUB_REF#refs/tags/v}
echo "version=$TAG" >> $GITHUB_OUTPUT
if [[ "$TAG" =~ -(alpha|beta|rc|canary) ]]; then
echo "is-prerelease=true" >> $GITHUB_OUTPUT
else
echo "is-prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Validate package.json version matches tag
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
TAG_VERSION="${{ steps.version.outputs.version }}"
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
echo "Package version ($PKG_VERSION) does not match tag ($TAG_VERSION)"
exit 1
fi
checks:
name: Run Checks
needs: validate
uses: ./.github/workflows/pr-checks.yml
build:
name: Build Package
needs: checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build package
run: npm run build
- name: Pack package
run: npm pack
- name: Upload package
uses: actions/upload-artifact@v4
with:
name: package
path: '*.tgz'
publish-npm:
name: Publish to NPM
needs: [validate, build]
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: npm
url: https://www.npmjs.com/package/@coditect/doc-viewer
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build package
run: npm run build
- name: Publish to npm (stable)
if: needs.validate.outputs.is-prerelease == 'false'
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to npm (pre-release)
if: needs.validate.outputs.is-prerelease == 'true'
run: |
TAG=$(echo "${{ needs.validate.outputs.version }}" | sed -n 's/.*-\([a-z]*\).*/\1/p')
npm publish --provenance --access public --tag $TAG
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
create-release:
name: Create GitHub Release
needs: [validate, publish-npm]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Generate changelog
id: changelog
run: |
npm install -g conventional-changelog-cli
CHANGELOG=$(conventional-changelog -p angular -r 2)
echo "$CHANGELOG" > RELEASE_NOTES.md
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
name: Release v${{ needs.validate.outputs.version }}
body_path: RELEASE_NOTES.md
prerelease: ${{ needs.validate.outputs.is-prerelease }}
draft: false
files: |
*.tgz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
update-docs:
name: Update Documentation Site
needs: [validate, publish-npm]
runs-on: ubuntu-latest
timeout-minutes: 10
if: needs.validate.outputs.is-prerelease == 'false'
steps:
- name: Trigger docs deployment
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: coditect-ai/documentation-site
event-type: package-published
client-payload: |
{
"package": "@coditect/doc-viewer",
"version": "${{ needs.validate.outputs.version }}"
}
notify:
name: Notify Release
needs: [validate, create-release]
runs-on: ubuntu-latest
if: always()
steps:
- name: Notify Slack
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Release v${{ needs.validate.outputs.version }} published",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rocket: *@coditect/doc-viewer v${{ needs.validate.outputs.version }}* published\n<https://www.npmjs.com/package/@coditect/doc-viewer|View on npm> | <${{ github.server_url }}/${{ github.repository }}/releases/tag/v${{ needs.validate.outputs.version }}|Release Notes>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Workflow 3: Canary
Triggers:
- Manual workflow dispatch from any branch
- Push to feature branches matching
feature/*
Jobs:
- Build package
- Publish with
--tag canary - Comment on PR with installation instructions
Complete YAML:
name: Canary Release
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to publish from'
required: true
default: 'main'
push:
branches:
- 'feature/**'
permissions:
contents: read
packages: write
pull-requests: write
id-token: write
jobs:
canary:
name: Publish Canary
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch || github.ref }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Run checks
run: |
npm run lint
npm run typecheck
npm run test:unit
- name: Build package
run: npm run build
- name: Generate canary version
id: version
run: |
BASE_VERSION=$(node -p "require('./package.json').version")
COMMIT_SHA=$(git rev-parse --short HEAD)
TIMESTAMP=$(date +%s)
CANARY_VERSION="${BASE_VERSION}-canary.${TIMESTAMP}.${COMMIT_SHA}"
echo "version=$CANARY_VERSION" >> $GITHUB_OUTPUT
npm version $CANARY_VERSION --no-git-tag-version
- name: Publish to npm (canary tag)
run: npm publish --provenance --access public --tag canary
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const version = '${{ steps.version.outputs.version }}';
const body = `
## Canary Release Published
Test this PR with:
\`\`\`bash
npm install @coditect/doc-viewer@${version}
\`\`\`
Or with canary tag:
\`\`\`bash
npm install @coditect/doc-viewer@canary
\`\`\`
**Note:** Canary releases are unstable and may be removed at any time.
`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
- name: Create deployment
uses: chrnorm/deployment-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
environment: canary
environment-url: https://www.npmjs.com/package/@coditect/doc-viewer/v/${{ steps.version.outputs.version }}
Dry Run Workflow
Purpose: Validate publish without actually publishing
name: Dry Run
on:
workflow_dispatch:
pull_request:
paths:
- 'package.json'
- '.github/workflows/release.yml'
jobs:
dry-run:
name: Publish Dry Run
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build package
run: npm run build
- name: Pack package
run: npm pack --dry-run
- name: Publish dry run
run: npm publish --dry-run
- name: Validate package contents
run: |
tar -tzf *.tgz | tee package-contents.txt
# Check required files
grep -q "package/dist/" package-contents.txt || (echo "Missing dist/" && exit 1)
grep -q "package/package.json" package-contents.txt || (echo "Missing package.json" && exit 1)
grep -q "package/README.md" package-contents.txt || (echo "Missing README.md" && exit 1)
# Check unwanted files are excluded
! grep -q "package/src/" package-contents.txt || (echo "Should not include src/" && exit 1)
! grep -q "package/node_modules/" package-contents.txt || (echo "Should not include node_modules/" && exit 1)
! grep -q "package/.git" package-contents.txt || (echo "Should not include .git/" && exit 1)
Automated Changelog Generation
Conventional Commits Enforcement
Commit Message Format:
<type>(<scope>): <subject>
<body>
<footer>
Valid Types:
| Type | SemVer Impact | Purpose | Examples |
|---|---|---|---|
feat | MINOR | New feature | feat(search): add fuzzy search support |
fix | PATCH | Bug fix | fix(viewer): resolve scroll position reset |
docs | PATCH | Documentation only | docs(api): update SearchBar props |
style | PATCH | Code style (no behavior change) | style: format with prettier |
refactor | PATCH | Code refactor (no behavior change) | refactor: extract search logic |
perf | PATCH | Performance improvement | perf(render): memoize component |
test | PATCH | Tests only | test(search): add fuzzy tests |
chore | PATCH | Build/tooling | chore: update dependencies |
ci | PATCH | CI/CD changes | ci: add bundle size check |
revert | Varies | Revert previous commit | revert: feat(search): add fuzzy search |
Breaking Changes:
# Add BREAKING CHANGE in footer
git commit -m "feat(api): rename theme prop
BREAKING CHANGE: theme prop renamed to colorScheme"
# Or add ! after type
git commit -m "feat(api)!: rename theme prop"
Scopes (optional but recommended):
feat(viewer): ... # DocumentViewer component
feat(search): ... # Search functionality
feat(nav): ... # Navigation
feat(theme): ... # Theming
feat(api): ... # Public API
feat(types): ... # TypeScript types
feat(cli): ... # CLI tool (create-doc-site)
Git Hook to Enforce:
.husky/commit-msg:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"
commitlint.config.js:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'chore',
'ci',
'revert'
]
],
'scope-enum': [
2,
'always',
[
'viewer',
'search',
'nav',
'theme',
'api',
'types',
'cli',
'deps'
]
],
'subject-case': [2, 'always', 'sentence-case'],
'header-max-length': [2, 'always', 100],
'body-max-line-length': [2, 'always', 200]
}
};
Changelog Generation with conventional-changelog
Install:
npm install -D conventional-changelog-cli conventional-changelog-conventionalcommits
package.json scripts:
{
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"changelog:all": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"version": "npm run changelog && git add CHANGELOG.md"
}
}
Configuration: .changelogrc.json:
{
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance Improvements" },
{ "type": "revert", "section": "Reverts" },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "style", "section": "Styles", "hidden": true },
{ "type": "chore", "section": "Chores", "hidden": true },
{ "type": "refactor", "section": "Code Refactoring", "hidden": true },
{ "type": "test", "section": "Tests", "hidden": true },
{ "type": "ci", "section": "CI/CD", "hidden": true }
],
"preMajor": false,
"commitUrlFormat": "https://github.com/coditect-ai/doc-viewer/commit/{{hash}}",
"compareUrlFormat": "https://github.com/coditect-ai/doc-viewer/compare/{{previousTag}}...{{currentTag}}",
"issueUrlFormat": "https://github.com/coditect-ai/doc-viewer/issues/{{id}}",
"userUrlFormat": "https://github.com/{{user}}"
}
Generated CHANGELOG.md Example:
# Changelog
## [1.2.0](https://github.com/coditect-ai/doc-viewer/compare/v1.1.0...v1.2.0) (2026-02-16)
### Features
* **search:** add fuzzy search support ([abc1234](https://github.com/coditect-ai/doc-viewer/commit/abc1234))
* **nav:** add breadcrumb navigation ([def5678](https://github.com/coditect-ai/doc-viewer/commit/def5678))
### Bug Fixes
* **viewer:** resolve scroll position reset on navigation ([9ab0123](https://github.com/coditect-ai/doc-viewer/commit/9ab0123))
* **theme:** fix dark mode contrast issues ([4cd5678](https://github.com/coditect-ai/doc-viewer/commit/4cd5678))
### Documentation
* **api:** update SearchBar props documentation ([ef01234](https://github.com/coditect-ai/doc-viewer/commit/ef01234))
## [1.1.0](https://github.com/coditect-ai/doc-viewer/compare/v1.0.0...v1.1.0) (2026-02-10)
### Features
* **theme:** add dark mode support ([123abc4](https://github.com/coditect-ai/doc-viewer/commit/123abc4))
Alternative: Changesets
Why Changesets:
- Better for monorepos
- Developer writes intent, not changelog
- Version bump determined from changesets
- More control over release notes
Install:
npm install -D @changesets/cli
npx changeset init
Configuration: .changeset/config.json:
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": [
"@changesets/changelog-github",
{
"repo": "coditect-ai/doc-viewer"
}
],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
Developer Workflow:
# 1. Make changes
git checkout -b feature/fuzzy-search
# 2. Create changeset
npx changeset
# Select: @coditect/doc-viewer
# Select: minor (new feature)
# Write: "Add fuzzy search support"
# 3. Commit changeset
git add .changeset/*.md
git commit -m "feat(search): add fuzzy search"
# 4. Open PR - changeset is committed
Generated Changeset: .changeset/fuzzy-search.md:
---
"@coditect/doc-viewer": minor
---
Add fuzzy search support
Implements fuzzy matching algorithm for search queries, improving user experience when searching documentation.
Release Process:
# 1. Consume changesets (updates package.json + CHANGELOG.md)
npx changeset version
# 2. Commit version changes
git add .
git commit -m "chore: version packages"
# 3. Publish
npx changeset publish
# 4. Push tags
git push --follow-tags
GitHub Action Integration:
name: Release (Changesets)
on:
push:
branches: [main]
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Create Release PR or Publish
uses: changesets/action@v1
with:
publish: npm run release
commit: 'chore: version packages'
title: 'Release: Version Packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Changesets vs Conventional Commits:
| Factor | Conventional Commits | Changesets |
|---|---|---|
| Setup | Simpler | More config |
| Developer UX | Just commit format | Extra changeset step |
| Control | Automatic | Manual changeset creation |
| Monorepo | Limited | Excellent |
| Versioning | Inferred from commits | Explicit in changeset |
| Release Notes | Auto-generated | Curated by developer |
Recommendation: Changesets for multi-package repos, Conventional Commits for single packages.
Quality Gates
Pre-Publish Checklist
All checks must pass before npm publish:
| Check | Tool | Threshold | Failure Action |
|---|---|---|---|
| Tests pass | Vitest | 100% | Block publish |
| Coverage | Vitest | ≥80% | Block publish |
| Type check | TypeScript | 0 errors | Block publish |
| Lint | ESLint | 0 errors | Block publish |
| Format | Prettier | No diff | Block publish |
| Bundle size | size-limit | ≤50KB (gzip) | Block publish |
| Security | npm audit | 0 high/critical | Block publish |
| Peer deps | npm ls | 0 conflicts | Block publish |
| Build | npm run build | Success | Block publish |
| Pack | npm pack | Success | Block publish |
1. Unit Tests
Tool: Vitest
Configuration: vitest.config.ts:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/*.test.{ts,tsx}',
'src/**/*.stories.{ts,tsx}',
'src/test/**'
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
}
}
});
package.json scripts:
{
"scripts": {
"test": "vitest",
"test:unit": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest watch"
}
}
CI Check:
- name: Run tests with coverage
run: npm run test:coverage
- name: Enforce coverage threshold
run: |
COVERAGE=$(node -p "Math.round(require('./coverage/coverage-summary.json').total.lines.pct)")
if [ "$COVERAGE" -lt 80 ]; then
echo "Coverage $COVERAGE% is below 80% threshold"
exit 1
fi
2. Integration Tests
Tool: Playwright
Configuration: playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/integration',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI
? [['github'], ['html']]
: [['list'], ['html']],
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI
}
});
Example Test:
import { test, expect } from '@playwright/test';
test('search functionality', async ({ page }) => {
await page.goto('/');
// Open search
await page.getByRole('button', { name: 'Search' }).click();
// Type query
await page.getByPlaceholder('Search documentation').fill('installation');
// Verify results
await expect(page.getByRole('list')).toContainText('Installation Guide');
// Navigate to result
await page.getByText('Installation Guide').click();
await expect(page).toHaveURL(/.*\/docs\/installation/);
});
3. Bundle Size
Tool: size-limit
Configuration: .size-limit.json:
[
{
"name": "DocumentViewer (ESM)",
"path": "dist/esm/index.js",
"import": "{ DocumentViewer }",
"limit": "50 KB",
"webpack": false,
"gzip": true
},
{
"name": "Full Bundle (ESM)",
"path": "dist/esm/index.js",
"limit": "120 KB",
"webpack": false,
"gzip": true
},
{
"name": "DocumentViewer (CJS)",
"path": "dist/cjs/index.js",
"import": "{ DocumentViewer }",
"limit": "55 KB",
"webpack": false,
"gzip": true
}
]
package.json scripts:
{
"scripts": {
"size": "size-limit",
"size:why": "size-limit --why"
}
}
CI Check with Comment:
- name: Check bundle size
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
limit: 50 KB
script: npm run size
Example PR Comment:
| Package | Size | Limit | Status |
|---------|------|-------|--------|
| DocumentViewer (ESM) | 48.2 KB | 50 KB | ✅ Pass |
| Full Bundle (ESM) | 115.8 KB | 120 KB | ✅ Pass |
| DocumentViewer (CJS) | 51.3 KB | 55 KB | ✅ Pass |
**Size Change:** +2.3 KB (+4.8%) compared to main
4. TypeScript Compilation
Configuration: tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
}
CI Check:
- name: Type check
run: npx tsc --noEmit --pretty
- name: Build declaration files
run: npx tsc --declaration --emitDeclarationOnly --outDir dist/types
5. Security Audit
npm audit:
# Audit dependencies
npm audit --audit-level=high
# Fix automatically
npm audit fix
# Manual review required
npm audit fix --force # May break things
Snyk Integration:
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high --fail-on=upgradable
Snyk Configuration: .snyk:
# Snyk (https://snyk.io) policy file
version: v1.25.0
# Ignore specific vulnerabilities
ignore:
'SNYK-JS-AXIOS-1038255':
- '*':
reason: 'Fixed in downstream update'
expires: '2026-03-16T00:00:00.000Z'
# Patch vulnerabilities
patch:
'npm:braces:20180219':
- 'micromatch > braces':
patched: '2026-02-16T10:30:00.000Z'
6. Peer Dependency Validation
Check peer dependency conflicts:
# List peer dependencies
npm ls --depth=0
# Check for peer dependency warnings
npm install --dry-run
CI Check:
- name: Validate peer dependencies
run: |
# Install peer deps for testing
npm install react@18.2.0 react-dom@18.2.0
# Run tests with peer deps
npm run test:unit
# Check for peer dep warnings
npm ls --depth=0 || exit 1
Script: scripts/check-peer-deps.js:
#!/usr/bin/env node
const { execSync } = require('child_process');
const pkg = require('../package.json');
const peerDeps = pkg.peerDependencies || {};
const peerDepsMeta = pkg.peerDependenciesMeta || {};
console.log('Checking peer dependencies...\n');
for (const [dep, range] of Object.entries(peerDeps)) {
const optional = peerDepsMeta[dep]?.optional;
try {
const version = execSync(`npm list ${dep} --depth=0 --json`, {
encoding: 'utf-8'
});
const parsed = JSON.parse(version);
const installed = parsed.dependencies?.[dep]?.version;
if (!installed) {
if (optional) {
console.log(`⚠️ Optional peer ${dep} not installed`);
} else {
console.error(`❌ Required peer ${dep} not found`);
process.exit(1);
}
} else {
console.log(`✅ ${dep}@${installed} satisfies ${range}`);
}
} catch (error) {
console.error(`❌ Failed to check ${dep}:`, error.message);
process.exit(1);
}
}
console.log('\nPeer dependency check passed!');
7. Package Contents Validation
Script: scripts/check-exports.js:
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const distDir = path.join(__dirname, '..', 'dist');
const pkg = require('../package.json');
console.log('Validating package exports...\n');
// Check main entry point
if (!fs.existsSync(path.join(distDir, pkg.main))) {
console.error(`❌ Main entry point missing: ${pkg.main}`);
process.exit(1);
}
console.log(`✅ Main: ${pkg.main}`);
// Check module entry point
if (pkg.module && !fs.existsSync(path.join(distDir, pkg.module))) {
console.error(`❌ Module entry point missing: ${pkg.module}`);
process.exit(1);
}
console.log(`✅ Module: ${pkg.module}`);
// Check types entry point
if (pkg.types && !fs.existsSync(path.join(distDir, pkg.types))) {
console.error(`❌ Types entry point missing: ${pkg.types}`);
process.exit(1);
}
console.log(`✅ Types: ${pkg.types}`);
// Check exports field
if (pkg.exports) {
for (const [exportPath, exportConfig] of Object.entries(pkg.exports)) {
if (typeof exportConfig === 'string') {
const fullPath = path.join(distDir, exportConfig);
if (!fs.existsSync(fullPath)) {
console.error(`❌ Export missing: ${exportPath} -> ${exportConfig}`);
process.exit(1);
}
console.log(`✅ Export: ${exportPath} -> ${exportConfig}`);
} else {
for (const [condition, conditionPath] of Object.entries(exportConfig)) {
const fullPath = path.join(distDir, conditionPath);
if (!fs.existsSync(fullPath)) {
console.error(`❌ Export missing: ${exportPath}.${condition} -> ${conditionPath}`);
process.exit(1);
}
console.log(`✅ Export: ${exportPath}.${condition} -> ${conditionPath}`);
}
}
}
}
console.log('\nPackage exports validation passed!');
CI Check:
- name: Validate package contents
run: |
npm pack --dry-run
node scripts/check-exports.js
# Check no dev files in package
npm pack
tar -tzf *.tgz | tee package-contents.txt
! grep -q "package/src/" package-contents.txt
! grep -q "package/.git" package-contents.txt
Publish Process
Manual Publish Workflow
Step-by-step process for maintainers:
1. Pre-Publish Preparation
# 1. Ensure main branch is up-to-date
git checkout main
git pull origin main
# 2. Run full test suite
npm run test:coverage
npm run test:integration
# 3. Build package
npm run build
# 4. Dry run publish
npm publish --dry-run
# 5. Check package contents
npm pack
tar -tzf *.tgz | less
2. Version Bump
Option A: npm version (automatic)
# Patch release (1.2.3 → 1.2.4)
npm version patch -m "chore: release v%s"
# Minor release (1.2.3 → 1.3.0)
npm version minor -m "chore: release v%s"
# Major release (1.2.3 → 2.0.0)
npm version major -m "chore: release v%s"
# Pre-release (1.2.3 → 1.2.4-beta.0)
npm version preminor --preid=beta -m "chore: release v%s"
Option B: Manual version update
# Edit package.json
vim package.json
# Commit version change
git add package.json
git commit -m "chore: release v1.2.4"
# Create git tag
git tag v1.2.4
3. Generate Changelog
# Generate changelog for new version
npm run changelog
# Review and edit CHANGELOG.md
vim CHANGELOG.md
# Commit changelog
git add CHANGELOG.md
git commit --amend --no-edit
4. Push to GitHub
# Push commits and tags
git push origin main --follow-tags
5. Wait for CI
# Monitor GitHub Actions
gh run watch
# Or check in browser
open https://github.com/coditect-ai/doc-viewer/actions
6. Verify Publish
# Check npm registry
npm view @coditect/doc-viewer
# Check specific version
npm view @coditect/doc-viewer@1.2.4
# Test installation
cd /tmp
npm init -y
npm install @coditect/doc-viewer@1.2.4
7. Verify GitHub Release
# Check GitHub release created
gh release view v1.2.4
# Or in browser
open https://github.com/coditect-ai/doc-viewer/releases/tag/v1.2.4
Automated Publish (CI/CD)
Trigger: Push tag matching v*.*.*
Process:
- Developer runs
npm version minor(creates commit + tag) - Developer pushes with
git push --follow-tags - GitHub Actions detects tag push
- CI runs all quality gates
- CI publishes to npm with provenance
- CI creates GitHub release with changelog
- CI triggers documentation site update
No manual npm publish required!
npm Provenance
What is provenance?
- Cryptographic attestation of package build source
- Proves package was built from specific GitHub commit
- Visible on npm package page as "Provenance" badge
Enable in CI:
- name: Publish with provenance
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Requirements:
- npm 9.5.0+
- GitHub Actions with
id-token: writepermission - Environment must be GitHub Actions (not local)
Verify provenance:
# Check package provenance
npm view @coditect/doc-viewer --json | jq .provenance
# Output:
{
"statement": {
"type": "https://in-toto.io/Statement/v0.1",
"subject": [{"name": "pkg:npm/@coditect/doc-viewer@1.2.4"}],
"predicateType": "https://slsa.dev/provenance/v0.2"
}
}
Post-Publish Checklist
Immediately after publish:
- Verify npm package page:
https://www.npmjs.com/package/@coditect/doc-viewer - Check provenance badge visible
- Verify GitHub release created
- Test installation:
npm install @coditect/doc-viewer@latest - Check documentation site updated
- Notify team in Slack/Discord
- Update downstream projects (if needed)
- Monitor npm downloads:
https://npmtrends.com/@coditect/doc-viewer
Within 24 hours:
- Monitor GitHub issues for version-specific bugs
- Check Dependabot alerts for new vulnerabilities
- Review npm audit results
- Respond to community feedback
Rollback Strategy
npm Unpublish Limitations
72-Hour Window:
- Can fully unpublish within 72 hours of publish
- After 72 hours: unpublish forbidden (npm policy)
- Rationale: prevent breaking downstream consumers
Unpublish Command:
# Unpublish specific version (within 72 hours)
npm unpublish @coditect/doc-viewer@1.2.4
# Unpublish entire package (dangerous!)
npm unpublish @coditect/doc-viewer --force
When to unpublish:
- Accidentally published wrong version
- Published with critical security vulnerability
- Published with broken build (won't install)
- Published with wrong scope/name
When NOT to unpublish:
- Minor bug (use deprecate + patch instead)
- Breaking change (should have been major version)
- After 72 hours (use deprecate)
npm Deprecate
For versions > 72 hours old:
# Deprecate specific version
npm deprecate @coditect/doc-viewer@1.2.4 "Critical bug - use v1.2.5 instead"
# Deprecate all versions in range
npm deprecate @coditect/doc-viewer@"1.2.x" "Upgrade to v1.3.0"
# Undeprecate
npm deprecate @coditect/doc-viewer@1.2.4 ""
Effect:
- Package still installable
- npm shows warning on install:
npm WARN deprecated @coditect/doc-viewer@1.2.4: Critical bug - use v1.2.5 instead - Visible on npm package page
Use cases:
- Known bugs in released version
- Security vulnerability (until patch available)
- Breaking change discovered post-release
Emergency Hotfix Process
Scenario: Critical bug in production version
1. Assess Severity
| Severity | Response | Timeframe |
|---|---|---|
| Critical | Immediate hotfix + deprecate | < 1 hour |
| High | Expedited patch release | < 4 hours |
| Medium | Next patch release | < 24 hours |
| Low | Next minor release | Next sprint |
Critical = Breaking production, security vulnerability, data loss
2. Hotfix Branch
# Create hotfix branch from published tag
git checkout -b hotfix/1.2.5 v1.2.4
# Fix the bug
vim src/buggy-file.tsx
# Commit fix
git commit -m "fix: resolve critical rendering bug"
# Version bump (patch)
npm version patch -m "chore: hotfix v%s"
# Push hotfix branch
git push origin hotfix/1.2.5 --follow-tags
3. Expedited Review
# Open PR
gh pr create --base main --head hotfix/1.2.5 --title "Hotfix: v1.2.5"
# Request immediate review
gh pr review --approve
# Merge without waiting for CI (if critical)
gh pr merge --squash
4. Deprecate Bad Version
# Deprecate while hotfix is building
npm deprecate @coditect/doc-viewer@1.2.4 "Critical bug - upgrade to v1.2.5"
5. Verify Hotfix
# Wait for CI publish
gh run watch
# Test installation
npm install @coditect/doc-viewer@latest
# Verify version
npm view @coditect/doc-viewer version
6. Notify Consumers
Post on GitHub Discussions:
## Critical Patch: v1.2.5
**Affected versions:** v1.2.4
**Issue:** Rendering bug causing crashes on search
**Fix:** Upgrade to v1.2.5:
\`\`\`bash
npm install @coditect/doc-viewer@latest
\`\`\`
**Deprecation:** v1.2.4 is now deprecated.
Notify via npm (in deprecation message):
npm deprecate @coditect/doc-viewer@1.2.4 \
"Critical rendering bug - upgrade to v1.2.5. See https://github.com/coditect-ai/doc-viewer/discussions/123"
Pinning Downstream Consumers
Prevent auto-update to bad version:
Option 1: Exact version in package.json
{
"dependencies": {
"@coditect/doc-viewer": "1.2.3"
}
}
Option 2: package-lock.json
# package-lock.json locks exact versions
npm ci # Use in CI to respect lock file
Option 3: npm shrinkwrap
# Generate npm-shrinkwrap.json (committed to git)
npm shrinkwrap
# Now all installs use exact versions
Advise consumers:
## Recommended Dependency Strategy
For production applications, pin major version:
\`\`\`json
{
"dependencies": {
"@coditect/doc-viewer": "^1.0.0"
}
}
\`\`\`
Use `package-lock.json` and `npm ci` in production.
Rollback Communication
Channels:
- GitHub Discussions (announcement)
- npm deprecation message
- Slack/Discord notification
- Twitter/social media (if widely used)
- Email to npm package subscribers (if available)
Template Message:
## Security Advisory: @coditect/doc-viewer v1.2.4
**Severity:** High
**Affected Versions:** v1.2.4
**Issue:** [Brief description of vulnerability]
**Mitigation:**
1. Upgrade to v1.2.5 immediately:
\`\`\`bash
npm install @coditect/doc-viewer@latest
\`\`\`
2. If upgrade not possible, pin to v1.2.3:
\`\`\`json
{
"dependencies": {
"@coditect/doc-viewer": "1.2.3"
}
}
\`\`\`
**Timeline:**
- 2026-02-16 10:00 UTC: v1.2.4 published
- 2026-02-16 14:30 UTC: Issue discovered
- 2026-02-16 15:00 UTC: v1.2.4 deprecated
- 2026-02-16 16:00 UTC: v1.2.5 hotfix published
**Credits:** [Security researcher name]
**Contact:** security@coditect.ai
Monitoring and Analytics
npm Download Statistics
API Endpoint:
# Daily downloads (last 7 days)
curl https://api.npmjs.org/downloads/point/last-week/@coditect/doc-viewer
# Range query
curl https://api.npmjs.org/downloads/range/2026-01-01:2026-02-16/@coditect/doc-viewer
# Response:
{
"downloads": 12543,
"start": "2026-02-09",
"end": "2026-02-16",
"package": "@coditect/doc-viewer"
}
Track with npm-stat:
npm install -g npm-stat
# View downloads
npm-stat @coditect/doc-viewer
# Output:
┌────────────┬───────────┐
│ Date │ Downloads │
├────────────┼───────────┤
│ 2026-02-16 │ 1,832 │
│ 2026-02-15 │ 2,145 │
│ 2026-02-14 │ 1,987 │
└────────────┴───────────┘
npmtrends.com:
https://npmtrends.com/@coditect/doc-viewer
Version Adoption
Script: scripts/check-adoption.js:
#!/usr/bin/env node
const fetch = require('node-fetch');
async function checkAdoption(packageName) {
// Get all versions
const pkg = await fetch(`https://registry.npmjs.org/${packageName}`).then(r => r.json());
// Get download stats for each version
const versions = Object.keys(pkg.versions);
const stats = await Promise.all(
versions.slice(-10).map(async (version) => {
const url = `https://api.npmjs.org/downloads/point/last-month/${packageName}`;
const data = await fetch(url).then(r => r.json());
return { version, downloads: data.downloads };
})
);
console.log('Version Adoption (last 30 days):\n');
stats.sort((a, b) => b.downloads - a.downloads).forEach(({ version, downloads }) => {
console.log(`${version.padEnd(10)} ${downloads.toLocaleString()} downloads`);
});
}
checkAdoption('@coditect/doc-viewer');
Dependabot Alerts
GitHub Dependabot monitors:
- Security vulnerabilities in dependencies
- Outdated dependencies
- Breaking changes in peer dependencies
Enable Dependabot:
.github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 10
reviewers:
- "coditect-ai/maintainers"
labels:
- "dependencies"
versioning-strategy: "increase"
# Group minor/patch updates
groups:
dependencies:
patterns:
- "*"
update-types:
- "minor"
- "patch"
Response to alerts:
- Review alert severity
- Test fix in feature branch
- Merge and publish patch version
- Notify consumers if breaking
Consumer Compatibility Tracking
Track which React versions are used:
Script: scripts/track-compatibility.js:
#!/usr/bin/env node
// Analyze package-lock.json from GitHub search results
// to understand which React versions consumers use
const fetch = require('node-fetch');
async function trackCompatibility() {
// Search GitHub for repos using our package
const query = encodeURIComponent('"@coditect/doc-viewer" filename:package.json');
const url = `https://api.github.com/search/code?q=${query}`;
const results = await fetch(url, {
headers: { 'Authorization': `token ${process.env.GITHUB_TOKEN}` }
}).then(r => r.json());
console.log(`Found ${results.total_count} repositories using @coditect/doc-viewer\n`);
// TODO: Fetch package.json from each repo and analyze React version
}
trackCompatibility();
Manual tracking in issues:
Create "Compatibility Reports" issue template:
---
name: Compatibility Report
about: Report compatibility with specific React/Node versions
labels: compatibility
---
**React Version:** 18.2.0
**Node Version:** 20.10.0
**@coditect/doc-viewer Version:** 1.2.4
**Status:** ✅ Works | ⚠️ Warnings | ❌ Broken
**Notes:**
[Any issues or warnings encountered]
Issue Triage
Labels for version-specific bugs:
labels:
- name: "v1.x"
color: "0E8A16"
description: "Affects v1.x versions"
- name: "v2.x"
color: "0E8A16"
description: "Affects v2.x versions"
- name: "breaking-change"
color: "D93F0B"
description: "Breaking API change"
- name: "needs-patch"
color: "FBCA04"
description: "Requires patch release"
- name: "regression"
color: "D93F0B"
description: "Worked in previous version"
Auto-label issues with GitHub Actions:
.github/workflows/label-issues.yml:
name: Label Issues
on:
issues:
types: [opened]
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const title = context.payload.issue.title.toLowerCase();
const body = context.payload.issue.body?.toLowerCase() || '';
const labels = [];
// Version-specific
if (body.includes('v1.')) labels.push('v1.x');
if (body.includes('v2.')) labels.push('v2.x');
// Bug type
if (title.includes('regression')) labels.push('regression');
if (title.includes('breaking')) labels.push('breaking-change');
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
labels: labels
});
}
Multi-Package Coordination
Publishing Order
Dependency relationship:
@coditect/doc-viewer (library)
↑
│ (scaffolds)
│
@coditect/create-doc-site (CLI)
Publishing order:
- Publish
@coditect/doc-viewerfirst - Wait for npm registry propagation (~5 minutes)
- Test installation:
npm install @coditect/doc-viewer@latest - Update
create-doc-sitetemplates to use new version - Publish
@coditect/create-doc-sitesecond
Why this order:
- CLI scaffolds projects with
doc-viewerdependency - If CLI is published first, it references non-existent
doc-viewerversion - Consumers run
npx @coditect/create-doc-siteand get install error
Version Alignment Strategy
Hybrid versioning (recommended):
@coditect/doc-viewer@2.5.3
@coditect/create-doc-site@2.1.0
Rules:
- Major versions always aligned (both v2.x.x)
- Minor/patch independent
- CLI scaffolds compatible range:
^2.0.0
When doc-viewer has breaking change:
- Bump
doc-viewerto v3.0.0 - Update CLI templates for v3 API
- Bump CLI to v3.0.0
- Publish both (doc-viewer first)
When doc-viewer has minor feature:
- Bump
doc-viewerto v2.6.0 - CLI stays at v2.1.0 (no change needed)
- Optionally: update CLI templates to use new feature → bump CLI to v2.2.0
Cross-Package Testing
Before publishing CLI:
# 1. Build and pack doc-viewer
cd packages/doc-viewer
npm run build
npm pack
# 2. Test CLI with packed doc-viewer
cd ../create-doc-site
npm install ../doc-viewer/coditect-doc-viewer-2.5.3.tgz
# 3. Run CLI locally
npm link
cd /tmp
create-doc-site my-test-docs
cd my-test-docs
npm install
npm run dev
# 4. Verify doc-viewer works in scaffolded project
CI workflow for cross-package testing:
name: Cross-Package Test
on:
pull_request:
paths:
- 'packages/doc-viewer/**'
- 'packages/create-doc-site/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build doc-viewer
run: |
cd packages/doc-viewer
npm ci
npm run build
npm pack
- name: Test create-doc-site with local doc-viewer
run: |
cd packages/create-doc-site
npm ci
npm install ../doc-viewer/*.tgz
npm test
- name: Test scaffolded project
run: |
cd /tmp
npx packages/create-doc-site test-docs
cd test-docs
npm install
npm run build
Template Version Management
In create-doc-site templates:
packages/create-doc-site/templates/default/package.json:
{
"name": "my-doc-site",
"version": "0.1.0",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@coditect/doc-viewer": "^2.0.0"
}
}
Version range rationale:
^2.0.0allows any v2.x.x (minor + patch updates)- Consumer gets latest v2 on first install
- Consumer controls updates via
package-lock.json
When to update template:
| doc-viewer change | Update template? | New CLI version? |
|---|---|---|
| Patch (2.5.3 → 2.5.4) | No | No |
| Minor (2.5.3 → 2.6.0) | Optional (if using new feature) | Minor (2.1.0 → 2.2.0) |
| Major (2.5.3 → 3.0.0) | Yes (API change) | Major (2.1.0 → 3.0.0) |
Monorepo Considerations
If packages move to monorepo:
Use Lerna or Nx:
# Initialize Lerna
npx lerna init
# Configure independent versioning
# lerna.json:
{
"version": "independent",
"npmClient": "npm",
"command": {
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish"
}
}
}
# Publish all changed packages
lerna publish
Or use Changesets (recommended):
# Install changesets
npm install -D @changesets/cli
npx changeset init
# Configure for monorepo
# .changeset/config.json:
{
"linked": [], # Independent versioning
"access": "public",
"baseBranch": "main",
"packages": ["packages/*"]
}
# Developer workflow
npx changeset # Create changeset
git add .changeset/*.md
git commit -m "feat: add new feature"
# Release workflow
npx changeset version # Bump versions
npx changeset publish # Publish to npm
Configuration Files
package.json (Complete Example)
@coditect/doc-viewer/package.json:
{
"name": "@coditect/doc-viewer",
"version": "1.2.4",
"description": "React component library for building beautiful documentation sites",
"keywords": [
"react",
"documentation",
"markdown",
"mdx",
"component-library",
"docs"
],
"homepage": "https://github.com/coditect-ai/doc-viewer#readme",
"bugs": {
"url": "https://github.com/coditect-ai/doc-viewer/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/coditect-ai/doc-viewer.git"
},
"license": "MIT",
"author": {
"name": "CODITECT",
"email": "engineering@coditect.ai",
"url": "https://coditect.ai"
},
"contributors": [
"Hal Casteel <hal@coditect.ai>"
],
"type": "module",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"default": "./dist/esm/index.js"
},
"./components": {
"types": "./dist/types/components/index.d.ts",
"import": "./dist/esm/components/index.js",
"require": "./dist/cjs/components/index.js"
},
"./hooks": {
"types": "./dist/types/hooks/index.d.ts",
"import": "./dist/esm/hooks/index.js",
"require": "./dist/cjs/hooks/index.js"
},
"./styles.css": "./dist/styles.css"
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"files": [
"dist",
"README.md",
"LICENSE",
"CHANGELOG.md"
],
"scripts": {
"dev": "vite",
"build": "npm run build:types && npm run build:esm && npm run build:cjs && npm run build:styles",
"build:types": "tsc --declaration --emitDeclarationOnly --outDir dist/types",
"build:esm": "tsc --module esnext --outDir dist/esm",
"build:cjs": "tsc --module commonjs --outDir dist/cjs",
"build:styles": "postcss src/styles/index.css -o dist/styles.css",
"test": "vitest",
"test:unit": "vitest run",
"test:coverage": "vitest run --coverage",
"test:integration": "playwright test",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,css}\"",
"typecheck": "tsc --noEmit",
"size": "size-limit",
"size:why": "size-limit --why",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"prepublishOnly": "npm run build && npm test && npm run typecheck",
"postpublish": "git push --follow-tags"
},
"dependencies": {
"clsx": "^2.1.0",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": false
},
"react-dom": {
"optional": false
}
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@playwright/test": "^1.41.0",
"@size-limit/preset-small-lib": "^11.0.2",
"@types/node": "^20.11.0",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.2.1",
"conventional-changelog-cli": "^4.1.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^9.0.6",
"jsdom": "^23.2.0",
"postcss": "^8.4.33",
"postcss-cli": "^11.0.0",
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"size-limit": "^11.0.2",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"vitest": "^1.2.1"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"sideEffects": [
"*.css"
]
}
.npmignore
What to exclude from npm package:
# Source files (only dist/ is published)
src/
*.test.ts
*.test.tsx
*.spec.ts
*.spec.tsx
__tests__/
test/
tests/
# Build tooling
.vite/
.turbo/
tsconfig.json
tsconfig.*.json
vite.config.ts
vitest.config.ts
playwright.config.ts
# Development files
.env
.env.*
.eslintrc.*
.prettierrc.*
.editorconfig
.nvmrc
# CI/CD
.github/
.gitlab-ci.yml
.circleci/
azure-pipelines.yml
# Git
.git/
.gitignore
.gitattributes
# Documentation (keep README.md)
docs/
*.md
!README.md
!CHANGELOG.md
# IDE
.vscode/
.idea/
*.swp
*.swo
*.sublime-*
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
yarn-debug.log*
# Dependencies
node_modules/
# Coverage
coverage/
.nyc_output/
# Misc
*.tgz
Alternative: Use files field in package.json (recommended):
{
"files": [
"dist",
"README.md",
"LICENSE",
"CHANGELOG.md"
]
}
Whitelist approach is safer - only explicitly listed files are included.
.npmrc (Project)
Committed to git:
# Registry
registry=https://registry.npmjs.org/
@coditect:registry=https://registry.npmjs.org/
# Publishing
access=public
sign-git-tag=true
git-tag-version=true
message="chore: release v%s"
# Package lock
package-lock=true
save-exact=false
save-prefix=^
# Security
audit=true
audit-level=high
# Performance
prefer-offline=true
fetch-retries=3
fetch-retry-mintimeout=10000
fetch-retry-maxtimeout=60000
tsconfig.json
Production TypeScript config:
{
"compilerOptions": {
// Output
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"outDir": "./dist",
// Declaration files
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// Module resolution
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
// Strictness
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Other
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true
},
"include": ["src"],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx"
]
}
Separate configs for ESM and CJS:
tsconfig.esm.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"outDir": "./dist/esm"
}
}
tsconfig.cjs.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./dist/cjs"
}
}
Build scripts:
{
"scripts": {
"build:esm": "tsc -p tsconfig.esm.json",
"build:cjs": "tsc -p tsconfig.cjs.json"
}
}
ESLint Configuration
eslint.config.js (ESLint 9 flat config):
import js from '@eslint/js';
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import prettier from 'eslint-config-prettier';
export default [
js.configs.recommended,
{
files: ['src/**/*.{ts,tsx}'],
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
}
},
plugins: {
'@typescript-eslint': tseslint,
'react': react,
'react-hooks': reactHooks
},
rules: {
...tseslint.configs.recommended.rules,
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
// TypeScript
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
// React
'react/react-in-jsx-scope': 'off', // Not needed with React 17+
'react/prop-types': 'off', // Use TypeScript instead
// Best practices
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'error'
},
settings: {
react: {
version: 'detect'
}
}
},
prettier // Must be last to override conflicting rules
];
Prettier Configuration
prettier.config.js:
export default {
semi: true,
trailingComma: 'es5',
singleQuote: true,
printWidth: 100,
tabWidth: 2,
useTabs: false,
arrowParens: 'always',
bracketSpacing: true,
endOfLine: 'lf'
};
Husky Git Hooks
Install:
npm install -D husky
npx husky init
.husky/pre-commit:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Run lint-staged
npx lint-staged
.husky/commit-msg:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Validate commit message
npx --no-install commitlint --edit "$1"
.husky/pre-push:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Run tests before push
npm run test:unit
lint-staged.config.js:
export default {
'*.{ts,tsx}': [
'eslint --fix',
'prettier --write',
'vitest related --run'
],
'*.{css,json,md}': [
'prettier --write'
]
};
Security and Access Control
npm Organization Setup
Create organization:
# 1. Login to npm
npm login
# 2. Create organization (via web UI)
open https://www.npmjs.com/org/create
# Or via CLI (requires npm 7+)
npm org create coditect
Add members:
# Add user as member
npm org set coditect:developers developer1
# Add user as owner
npm org set coditect:owners owner1
# List members
npm org ls coditect
Remove members:
npm org rm coditect developer1
npm Token Management
Token types:
| Type | Use Case | 2FA Required | CIDR Restriction | Expiry |
|---|---|---|---|---|
| Read-only | Testing private packages | No | Optional | Never |
| Automation | CI/CD publishing | No | Recommended | 1 year |
| Publish | Manual publishing | Yes (if enabled) | No | 90 days |
Create automation token:
# 1. Create token (web UI recommended)
open https://www.npmjs.com/settings/~/tokens
# Or via CLI
npm token create --read-write --cidr=0.0.0.0/0
# 2. Save token securely
# DO NOT commit to git!
# 3. Store in GitHub Secrets
gh secret set NPM_TOKEN --body "npm_xxxxxxxxx"
Restrict token by CIDR (recommended for CI):
# Restrict to GitHub Actions IPs
npm token create --read-write --cidr=192.30.252.0/22,185.199.108.0/22,140.82.112.0/20
# Or restrict to specific VPN
npm token create --read-write --cidr=10.0.0.0/8
List tokens:
npm token list
Revoke token:
npm token revoke <token-id>
Token rotation (annually):
# 1. Create new token
NEW_TOKEN=$(npm token create --read-write --cidr=0.0.0.0/0 --json | jq -r .token)
# 2. Update GitHub secret
gh secret set NPM_TOKEN --body "$NEW_TOKEN"
# 3. Test new token
echo "//registry.npmjs.org/:_authToken=$NEW_TOKEN" > ~/.npmrc-test
npm whoami --registry https://registry.npmjs.org/ --userconfig ~/.npmrc-test
# 4. Revoke old token
npm token revoke <old-token-id>
Two-Factor Authentication
Enable 2FA for npm account:
# Enable auth-only mode (required for login)
npm profile enable-2fa auth-only
# Enable auth-and-writes mode (required for publish)
npm profile enable-2fa auth-and-writes
2FA modes:
| Mode | Login | Publish | Password Change |
|---|---|---|---|
| auth-only | 2FA required | No 2FA | 2FA required |
| auth-and-writes | 2FA required | 2FA required | 2FA required |
Publish with 2FA:
# Manual publish requires OTP
npm publish --otp=123456
# Automation tokens bypass 2FA
# (Use auth-only mode + automation token for CI)
Backup codes:
# Generate backup codes (use if lose authenticator)
npm profile enable-2fa auth-and-writes
# Save backup codes securely!
GitHub Actions Permissions
Workflow permissions:
name: Release
on:
push:
tags: ['v*.*.*']
permissions:
contents: write # Create releases
packages: write # Publish to GitHub Packages
id-token: write # npm provenance
jobs:
publish:
runs-on: ubuntu-latest
environment:
name: npm
url: https://www.npmjs.com/package/@coditect/doc-viewer
steps:
- name: Publish
run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Environment protection rules:
# GitHub repo settings → Environments → npm
Protection rules:
- Required reviewers: @coditect-ai/maintainers
- Wait timer: 5 minutes
- Deployment branches: tags matching v*.*.*
Supply Chain Security
Lockfile integrity:
# Generate lockfile
npm install
# Verify lockfile integrity
npm ci # Fails if package-lock.json is out of sync
# Check for corruption
npm audit signatures
Dependency provenance:
# Check dependency provenance (npm 9+)
npm view react --json | jq .provenance
# Verify package signatures
npm audit signatures
Subresource Integrity (SRI):
<!-- If serving from CDN, use SRI hashes -->
<script
src="https://unpkg.com/@coditect/doc-viewer@1.2.4/dist/doc-viewer.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
crossorigin="anonymous"
></script>
Generate SRI hash:
curl -s https://unpkg.com/@coditect/doc-viewer@1.2.4/dist/doc-viewer.min.js | \
openssl dgst -sha384 -binary | \
openssl base64 -A
Security Scanning
npm audit:
# Audit dependencies
npm audit
# Fix automatically
npm audit fix
# Audit production only
npm audit --production
# Set severity threshold
npm audit --audit-level=high
Snyk:
# Install Snyk CLI
npm install -g snyk
# Authenticate
snyk auth
# Test for vulnerabilities
snyk test
# Monitor project (continuous scanning)
snyk monitor
# Fix vulnerabilities
snyk fix
GitHub Dependabot:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
Troubleshooting
Common Publish Errors
1. E403 Forbidden
Error:
npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/@coditect%2fdoc-viewer
npm ERR! 403 You do not have permission to publish "@coditect/doc-viewer"
Causes:
- Not logged in to npm
- Wrong npm account
- Not member of @coditect org
- Token expired or invalid
- Package name taken
Solutions:
# Check current user
npm whoami
# Login with correct account
npm logout
npm login
# Check org membership
npm org ls coditect
# Verify token (in CI)
echo $NPM_TOKEN | head -c 20 # Should start with "npm_"
# Try different package name
npm search @coditect/doc-viewer # Check if taken
2. E402 Payment Required
Error:
npm ERR! 402 Payment Required - PUT https://registry.npmjs.org/@coditect%2fdoc-viewer
npm ERR! 402 You must sign up for private packages
Cause:
- Trying to publish scoped package as private without paid plan
Solution:
# Publish as public
npm publish --access public
# Or add to package.json
{
"publishConfig": {
"access": "public"
}
}
3. E409 Conflict
Error:
npm ERR! 409 Conflict - PUT https://registry.npmjs.org/@coditect%2fdoc-viewer
npm ERR! 409 Cannot publish over the previously published versions: 1.2.4
Cause:
- Version 1.2.4 already published
- Cannot overwrite published versions
Solution:
# Bump version
npm version patch # 1.2.4 → 1.2.5
# Republish
npm publish
4. ENEEDAUTH
Error:
npm ERR! code ENEEDAUTH
npm ERR! need auth This command requires you to be logged in
Cause:
- No authentication token in .npmrc
Solution:
# Login
npm login
# Or set token manually
echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" >> .npmrc
# In CI, ensure NPM_TOKEN is set
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
5. EOTP
Error:
npm ERR! code EOTP
npm ERR! publish This operation requires a one-time password
Cause:
- 2FA enabled, OTP required
Solution:
# Publish with OTP
npm publish --otp=123456
# Or use automation token (bypasses 2FA)
# Create automation token in npm settings
Build Errors
1. TypeScript Errors
Error:
src/index.ts:10:5 - error TS2322: Type 'string' is not assignable to type 'number'
Solution:
# Fix type errors
vim src/index.ts
# Re-run typecheck
npm run typecheck
# Build
npm run build
2. Missing Dependencies
Error:
Module not found: Error: Can't resolve 'react'
Solution:
# Install missing dependencies
npm install react react-dom
# Or install all dependencies
npm ci
3. ESM/CJS Compatibility
Error:
require() of ES Module not supported
Solution:
Build both ESM and CJS:
{
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
}
}
Or use tsup for dual builds:
npm install -D tsup
# tsup.config.ts
export default {
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
splitting: false,
sourcemap: true,
clean: true
};
CI/CD Errors
1. GitHub Actions Timeout
Error:
Error: The job running on runner GitHub Actions X has exceeded the maximum execution time of 360 minutes
Solution:
# Reduce timeout
jobs:
build:
timeout-minutes: 30 # Default is 360
# Or optimize build
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
2. Provenance Errors
Error:
npm ERR! Provenance generation failed
Solution:
# Ensure permissions are set
permissions:
contents: read
id-token: write # Required for provenance
# Use supported Node.js version
- uses: actions/setup-node@v4
with:
node-version: '20' # npm 9+ required
3. GitHub Release Creation Fails
Error:
Error: Resource not accessible by integration
Solution:
# Ensure permissions
permissions:
contents: write # Required for releases
# Use GITHUB_TOKEN
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Registry Issues
1. npm Registry Down
Error:
npm ERR! code ECONNREFUSED
npm ERR! errno ECONNREFUSED
npm ERR! FetchError: request to https://registry.npmjs.org failed
Solution:
# Check npm status
open https://status.npmjs.org
# Use alternate registry (temporary)
npm config set registry https://registry.npmmirror.com
# Reset to default
npm config delete registry
2. Package Not Found After Publish
Error:
npm ERR! 404 Not Found - GET https://registry.npmjs.org/@coditect%2fdoc-viewer
npm ERR! 404 '@coditect/doc-viewer@latest' is not in this registry
Cause:
- Registry propagation delay (usually < 1 minute)
Solution:
# Wait 1-2 minutes, then retry
sleep 120
npm install @coditect/doc-viewer@latest
# Or check specific registry
npm view @coditect/doc-viewer --registry https://registry.npmjs.org
Rollback Issues
1. Cannot Unpublish (> 72 hours)
Error:
npm ERR! 403 Forbidden - DELETE https://registry.npmjs.org/@coditect%2fdoc-viewer/-/doc-viewer-1.2.4.tgz
npm ERR! 403 This package version cannot be unpublished because it was published more than 72 hours ago
Solution:
# Use deprecate instead
npm deprecate @coditect/doc-viewer@1.2.4 "Use v1.2.5 instead - critical bug"
# Publish fixed version
npm version patch
npm publish
Appendix
Checklist: First-Time Publish
Before first publish:
- Create npm account
- Create @coditect organization
- Add team members to org
- Configure 2FA (auth-only for CI, auth-and-writes for maintainers)
- Create automation token
- Store token in GitHub Secrets (
NPM_TOKEN) - Configure package.json (name, version, description, keywords, etc.)
- Add LICENSE file (MIT recommended)
- Write README.md with installation and usage examples
- Configure .npmignore or
filesfield - Set up TypeScript build (ESM + CJS)
- Configure ESLint and Prettier
- Set up Vitest for testing
- Configure GitHub Actions workflows (PR checks, release, canary)
- Test build:
npm run build - Test pack:
npm packand inspect .tgz - Dry run:
npm publish --dry-run - Publish:
npm publish --access public - Verify:
npm view @coditect/doc-viewer - Test install:
npm install @coditect/doc-viewer
Useful npm Commands
# View package info
npm view @coditect/doc-viewer
npm view @coditect/doc-viewer versions
npm view @coditect/doc-viewer dist-tags
# Check what will be published
npm pack --dry-run
# List files in published package
npm pack
tar -tzf *.tgz
# Check package size
npm publish --dry-run --json | jq '.size'
# Deprecate version
npm deprecate @coditect/doc-viewer@1.2.4 "Use v1.2.5"
# Add/update dist-tag
npm dist-tag add @coditect/doc-viewer@1.3.0 latest
npm dist-tag add @coditect/doc-viewer@2.0.0-beta.1 next
# List dist-tags
npm dist-tag ls @coditect/doc-viewer
# Check who can publish
npm owner ls @coditect/doc-viewer
# Add collaborator
npm owner add username @coditect/doc-viewer
GitHub CLI Commands
# Watch workflow run
gh run watch
# List recent runs
gh run list --workflow=release.yml
# View run logs
gh run view <run-id> --log
# Trigger workflow manually
gh workflow run release.yml
# Create release
gh release create v1.2.4 --title "v1.2.4" --notes-file RELEASE_NOTES.md
# List releases
gh release list
# Delete release
gh release delete v1.2.4
References
- npm CLI Documentation
- npm Publishing Guide
- npm Provenance
- Semantic Versioning 2.0.0
- Conventional Commits
- GitHub Actions: Publishing Node.js packages
- Changesets Documentation
- size-limit
- Playwright Testing
- Vitest Testing Framework
Document Version: 1.0.0 Created: 2026-02-16 Author: Claude (Sonnet 4.5) Task: A.6.3 Status: Complete
End of Document