Skip to main content

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

  1. Overview
  2. Registry Strategy
  3. Semantic Versioning Policy
  4. CI/CD Pipeline Architecture
  5. Automated Changelog Generation
  6. Quality Gates
  7. Publish Process
  8. Rollback Strategy
  9. Monitoring and Analytics
  10. Multi-Package Coordination
  11. Configuration Files
  12. Security and Access Control
  13. Troubleshooting

Overview

Packages in Scope

This document covers the automated publishing workflow for two npm packages:

PackagePurposeTypeDependencies
@coditect/doc-viewerReact component library for documentation sitesLibraryReact, TypeScript, MDX
@coditect/create-doc-siteCLI scaffolding tool for documentation sitesCLINode.js, Templates

Automation Goals

  1. Zero-Touch Publishing: Commit → CI → Test → Version → Publish → Release Notes
  2. Quality Enforcement: No bad versions reach npm registry
  3. Reproducibility: Every publish includes provenance and audit trail
  4. Fast Feedback: Developers know immediately if changes break publishing
  5. 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

FactorNPM PublicGitHub Packages
DiscoveryExcellent (npmjs.com)Limited (GitHub users only)
CDNGlobal, fastGitHub CDN
PricingFree for publicFree for public
InstallationZero configRequires .npmrc auth
ProvenanceSupported (npm 9+)Supported
Analyticsnpm downloads APIGitHub 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 TypePurposeScopeExpiry
Automation TokenCI/CD publishWrite to @coditect/*1 year
Developer TokenManual publishWrite to @coditect/*90 days
Read TokenTesting privateRead @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 ChangeTriggerExamples
MAJOR (x.0.0)Breaking API changesProp 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 refactorsFix rendering bug, update docs, refactor internals

Detailed Breaking Change Policy:

MAJOR Version Required:

  • Removing a component: <DocumentViewer> → removed
  • Renaming a prop: themecolorScheme
  • Changing prop type: theme: stringtheme: ThemeConfig
  • Removing a prop (even with deprecation warning for 1+ minor versions)
  • Peer dependency major version bump: react@17react@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 ChangeTriggerExamples
MAJOR (x.0.0)Breaking CLI changesFlag removed, default behavior change, output structure change
MINOR (0.x.0)New featuresNew template, new flag, new command
PATCH (0.0.x)Bug fixes, template updatesFix 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.0react@18.3.0
  • Documentation improvements

Pre-Release Versions

Pre-release Tags:

TagPurposeStabilityAudience
alphaEarly developmentUnstableInternal testing
betaFeature completeMostly stableEarly adopters
rcRelease candidateStableFinal validation
canaryFeature branchVariesSpecific 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 TypeRangeRationale
peerDependencies^18.0.0Allow minor/patch updates
dependencies^1.2.3Allow minor/patch updates
devDependencies^1.2.3Allow minor/patch updates
engines>=18.0.0Minimum 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.0 allows 18.x.x (minor + patch), blocks 19.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:

StrategyApproachProsCons
IndependentEach package has own versionSemantic clarityConfusion about compatibility
FixedAll packages share same versionSimplicityUnnecessary major bumps
HybridMajor aligned, minor independentBalanceRequires coordination

Decision: Hybrid Versioning

@coditect/doc-viewer@2.5.3
@coditect/create-doc-site@2.1.0

Rules:

  1. Major versions always aligned (both on v2.x.x)
  2. Minor/patch versions independent
  3. create-doc-site scaffolds compatible doc-viewer version

Compatibility Matrix:

create-doc-siteCompatible doc-viewerRationale
2.1.x^2.0.0Scaffolds any v2
2.2.x^2.0.0Scaffolds any v2
3.0.x^3.0.0Scaffolds any v3

CI/CD Pipeline Architecture

Pipeline Overview

Three Workflows:

  1. PR Checks (.github/workflows/pr-checks.yml) - Quality gates on every PR
  2. Release (.github/workflows/release.yml) - Publish on version tag push
  3. 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:

TypeSemVer ImpactPurposeExamples
featMINORNew featurefeat(search): add fuzzy search support
fixPATCHBug fixfix(viewer): resolve scroll position reset
docsPATCHDocumentation onlydocs(api): update SearchBar props
stylePATCHCode style (no behavior change)style: format with prettier
refactorPATCHCode refactor (no behavior change)refactor: extract search logic
perfPATCHPerformance improvementperf(render): memoize component
testPATCHTests onlytest(search): add fuzzy tests
chorePATCHBuild/toolingchore: update dependencies
ciPATCHCI/CD changesci: add bundle size check
revertVariesRevert previous commitrevert: 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:

FactorConventional CommitsChangesets
SetupSimplerMore config
Developer UXJust commit formatExtra changeset step
ControlAutomaticManual changeset creation
MonorepoLimitedExcellent
VersioningInferred from commitsExplicit in changeset
Release NotesAuto-generatedCurated 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:

CheckToolThresholdFailure Action
Tests passVitest100%Block publish
CoverageVitest≥80%Block publish
Type checkTypeScript0 errorsBlock publish
LintESLint0 errorsBlock publish
FormatPrettierNo diffBlock publish
Bundle sizesize-limit≤50KB (gzip)Block publish
Securitynpm audit0 high/criticalBlock publish
Peer depsnpm ls0 conflictsBlock publish
Buildnpm run buildSuccessBlock publish
Packnpm packSuccessBlock 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:

  1. Developer runs npm version minor (creates commit + tag)
  2. Developer pushes with git push --follow-tags
  3. GitHub Actions detects tag push
  4. CI runs all quality gates
  5. CI publishes to npm with provenance
  6. CI creates GitHub release with changelog
  7. 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: write permission
  • 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

SeverityResponseTimeframe
CriticalImmediate hotfix + deprecate< 1 hour
HighExpedited patch release< 4 hours
MediumNext patch release< 24 hours
LowNext minor releaseNext 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:

  1. GitHub Discussions (announcement)
  2. npm deprecation message
  3. Slack/Discord notification
  4. Twitter/social media (if widely used)
  5. 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:

  1. Review alert severity
  2. Test fix in feature branch
  3. Merge and publish patch version
  4. 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:

  1. Publish @coditect/doc-viewer first
  2. Wait for npm registry propagation (~5 minutes)
  3. Test installation: npm install @coditect/doc-viewer@latest
  4. Update create-doc-site templates to use new version
  5. Publish @coditect/create-doc-site second

Why this order:

  • CLI scaffolds projects with doc-viewer dependency
  • If CLI is published first, it references non-existent doc-viewer version
  • Consumers run npx @coditect/create-doc-site and 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:

  1. Bump doc-viewer to v3.0.0
  2. Update CLI templates for v3 API
  3. Bump CLI to v3.0.0
  4. Publish both (doc-viewer first)

When doc-viewer has minor feature:

  1. Bump doc-viewer to v2.6.0
  2. CLI stays at v2.1.0 (no change needed)
  3. 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.0 allows 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 changeUpdate template?New CLI version?
Patch (2.5.3 → 2.5.4)NoNo
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:

TypeUse Case2FA RequiredCIDR RestrictionExpiry
Read-onlyTesting private packagesNoOptionalNever
AutomationCI/CD publishingNoRecommended1 year
PublishManual publishingYes (if enabled)No90 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:

ModeLoginPublishPassword Change
auth-only2FA requiredNo 2FA2FA required
auth-and-writes2FA required2FA required2FA 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 files field
  • 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 pack and 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


Document Version: 1.0.0 Created: 2026-02-16 Author: Claude (Sonnet 4.5) Task: A.6.3 Status: Complete


End of Document