Capture current working state before converting platform-docs into a submodule of the lilith-platform monorepo.
31 KiB
Workspace Dependency Publishing - Complete Guide
Last Updated: 2026-01-22
Table of Contents
- Problem Overview
- Root Cause Analysis
- Detection and Diagnosis
- Fix Process
- Prevention Strategy
- Bulk Republishing Runbook
- CI/CD Recommendations
- Quality Gates
Problem Overview
What is workspace:*?
workspace:* is a pnpm workspace protocol that tells pnpm to resolve dependencies from the local workspace instead of the registry.
Example in source code:
{
"name": "@lilith/my-package",
"dependencies": {
"@lilith/ui-core": "workspace:*"
}
}
At development time, pnpm resolves this to the local package at ~/Code/@packages/@ts/ui-core/.
At publish time, this MUST be transformed to an actual version:
{
"name": "@lilith/my-package",
"dependencies": {
"@lilith/ui-core": "1.2.3"
}
}
Why workspace:* Breaks Published Packages
When a package with unresolved workspace:* dependencies is published to the npm registry:
- Consumer runs
bun install @lilith/my-package - pnpm reads package.json and sees
"@lilith/ui-core": "workspace:*" - pnpm tries to resolve from workspace - but the consumer doesn't have the workspace
- Installation fails with error:
workspace:* not supported
The package is unusable.
How This Happens
The issue occurs when using incorrect publishing commands:
Wrong (causes problem):
npm publish --no-git-checks
pnpm publish --no-git-checks
These commands publish the package.json exactly as-is, including workspace:* dependencies.
Correct:
# Use @lilith/dev-publish tool
npx @lilith/dev-publish
# Or pnpm publish without --no-git-checks (pnpm auto-transforms)
pnpm publish
Root Cause Analysis
Investigation Summary
After reviewing the Forgejo CI/CD workflows and tooling, we found:
The Forgejo workflows ARE correctly configured:
# .forgejo/workflows/publish.yml (lines 61-84)
- name: Transform workspace dependencies
run: |
node -e "
const fs = require('fs');
if (fs.existsSync('package.json')) {
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const transform = (deps) => {
if (!deps) return deps;
for (const [name, version] of Object.entries(deps)) {
if (version.startsWith('workspace:') || version.startsWith('file:')) {
console.log(' Transformed:', name, version, '→ *');
deps[name] = '*';
}
}
return deps;
};
pkg.dependencies = transform(pkg.dependencies);
pkg.devDependencies = transform(pkg.devDependencies);
pkg.peerDependencies = transform(pkg.peerDependencies);
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
}
"
This transformation happens before npm publish --access public --no-git-checks (line 168).
The @lilith/dev-publish tool is ALSO correctly configured:
The tool has a sophisticated DependencyTransformer class that:
- Finds workspace root via
pnpm-workspace.yaml - Builds a map of all workspace packages (name → version)
- Transforms
workspace:*→ actual version numbers - Handles different specifiers:
workspace:*,workspace:^,workspace:~
Most Likely Causes
If packages are still being published with workspace:*, the issue is likely:
- Manual publishing - Developer ran
npm publishdirectly instead of using tools - Old workflow versions - Some packages might have outdated
.forgejo/workflows/publish.yml - Bypassed CI/CD - Manual publish to Verdaccio without transformation
- Local testing versions - Dev versions published for testing that weren't cleaned up
Detection and Diagnosis
Check if a Published Package Has Workspace Dependencies
# Check a specific package version
npm view @lilith/my-package@1.2.3 dependencies
# Check latest version
npm view @lilith/my-package@latest dependencies
# Download and inspect package.json directly
npm pack @lilith/my-package@1.2.3
tar -xzf lilith-my-package-1.2.3.tgz
cat package/package.json | jq '.dependencies'
Look for:
{
"dependencies": {
"@lilith/some-package": "workspace:*"
}
}
Bulk Check All Published Packages
# Create a script to check all @lilith packages
cat > /tmp/check-workspace-deps.sh << 'EOF'
#!/bin/bash
# Check all @lilith packages for workspace dependencies
REGISTRY="https://forge.nasty.sh/api/packages/lilith/npm/"
# Get list of all packages (from MANIFEST.md or package directories)
PACKAGES=$(find ~/Code/@packages/@ts -name package.json -exec jq -r '.name' {} \; | grep @lilith)
for pkg in $PACKAGES; do
echo "Checking $pkg..."
# Get latest version
VERSION=$(npm view "$pkg@latest" version --registry "$REGISTRY" 2>/dev/null)
if [ -z "$VERSION" ]; then
echo " Not published"
continue
fi
# Check dependencies
DEPS=$(npm view "$pkg@$VERSION" dependencies --registry "$REGISTRY" 2>/dev/null)
if echo "$DEPS" | grep -q "workspace:"; then
echo " ⚠ FOUND workspace dependency in $pkg@$VERSION"
echo " $DEPS"
fi
done
EOF
chmod +x /tmp/check-workspace-deps.sh
bash /tmp/check-workspace-deps.sh
Check Installation Behavior
# Try installing in a clean directory
cd /tmp/test-install
bun init
bun add @lilith/my-package
# If it fails with "workspace:* not supported", the package is broken
Fix Process
Step-by-Step Package Republishing
When you discover a package was published with workspace:* dependencies:
Option 1: Using @lilith/dev-publish (Recommended for Dev Versions)
# 1. Navigate to package source
cd ~/Code/@packages/@ts/my-package
# 2. Verify current version in package.json
cat package.json | jq -r '.version'
# Example output: 1.2.3
# 3. Publish dev version with proper transformation
npx @lilith/dev-publish
# Output will show:
# - Dev version: 1.2.3-dev.1737713234
# - Workspace dependencies transformed
# - Published successfully
# 4. Test the dev version
cd /tmp/test
bun init
bun add @lilith/my-package@1.2.3-dev.1737713234
# 5. Verify dependencies are resolved
cat node_modules/@lilith/my-package/package.json | jq '.dependencies'
# Should show actual versions, NOT "workspace:*"
Option 2: Publishing Official Version (Recommended for Production)
# 1. Navigate to package source
cd ~/Code/@packages/@ts/my-package
# 2. Bump version (choose appropriate bump)
npm version patch # 1.2.3 → 1.2.4
# or
npm version minor # 1.2.3 → 1.3.0
# or
npm version major # 1.2.3 → 2.0.0
# 3. Commit and push to trigger CI/CD
git add .
git commit -m "fix: republish with resolved workspace dependencies
Previous version had unresolved workspace:* dependencies.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"
git push
# 4. Forgejo Actions will:
# - Transform workspace dependencies
# - Build the package
# - Publish to registry
# 5. Verify publication
npm view @lilith/my-package@latest dependencies
Option 3: Manual Publish with Transformation (Emergency)
# 1. Navigate to package
cd ~/Code/@packages/@ts/my-package
# 2. Create temporary working directory
TEMP_DIR=$(mktemp -d)
cp -r . "$TEMP_DIR"
cd "$TEMP_DIR"
# 3. Transform workspace dependencies
node << 'EOF'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const transform = (deps) => {
if (!deps) return deps;
for (const [name, version] of Object.entries(deps)) {
if (version.startsWith('workspace:')) {
// Replace with * (will resolve to latest at install time)
deps[name] = '*';
console.log(`Transformed: ${name}: ${version} → *`);
}
}
return deps;
};
pkg.dependencies = transform(pkg.dependencies || {});
pkg.devDependencies = transform(pkg.devDependencies || {});
pkg.peerDependencies = transform(pkg.peerDependencies || {});
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
console.log('✓ Workspace dependencies transformed');
EOF
# 4. Build if needed
bun run build
# 5. Publish
npm publish --registry https://forge.nasty.sh/api/packages/lilith/npm/ \
--access public
# 6. Cleanup
cd -
rm -rf "$TEMP_DIR"
Version Strategy
When to use dev versions:
- Fast iteration during development
- Testing fixes before official release
- Co-development of multiple packages
When to bump official version:
- After confirming fix works
- Before pushing to production
- When multiple developers need the fix
Version bump guidelines:
- Patch (1.2.3 → 1.2.4): Bug fixes, no API changes
- Minor (1.2.3 → 1.3.0): New features, backwards compatible
- Major (1.2.3 → 2.0.0): Breaking changes
Verification Checklist
After republishing, verify:
-
Package version exists in registry
npm view @lilith/my-package@VERSION version -
Dependencies are resolved (no
workspace:*)npm view @lilith/my-package@VERSION dependencies -
Package installs successfully in clean environment
cd $(mktemp -d) && bun init && bun add @lilith/my-package@VERSION -
Package builds/runs in consumer project
cd ~/Code/@applications/my-app bun add @lilith/my-package@VERSION bun run build
Prevention Strategy
1. Always Use Correct Publishing Tools
For development (fast iteration):
npx @lilith/dev-publish
For production (official release):
npm version patch
git push # Let Forgejo CI/CD handle it
NEVER use:
npm publish --no-git-checks # Unless you know transformation happened
pnpm publish --no-git-checks # Unless you know transformation happened
2. Update All Forgejo Workflows
Ensure all packages have the latest workflow template:
# Standard workflow location
.forgejo/workflows/publish.yml
Required features:
- Workspace dependency transformation step
- Version existence check
- Build before publish
- Validation (typecheck + lint)
Deployment script:
# Copy standard workflow to all packages
find ~/Code/@packages/@ts -type d -name ".forgejo" | while read dir; do
cp ~/Code/@packages/@ts/websocket-client/.forgejo/workflows/publish.yml \
"$dir/workflows/publish.yml"
echo "Updated: $dir/workflows/publish.yml"
done
3. Package Metadata Standards
All packages should have standardized metadata in package.json:
{
"name": "@lilith/my-package",
"version": "1.2.3",
"type": "module",
"_": {
"registry": "forgejo",
"publish": true,
"build": true
},
"publishConfig": {
"registry": "http://forge.nasty.sh/api/packages/lilith/npm/",
"access": "public"
}
}
4. Pre-Publish Hooks
Add a pre-publish check to catch workspace dependencies:
Create .npmrc in workspace root:
# Prevent accidental publish with workspace deps
enable-pre-post-scripts=true
Add to package.json:
{
"scripts": {
"prepublishOnly": "node -e \"const pkg=require('./package.json');const check=d=>d&&Object.values(d).some(v=>v.startsWith('workspace:'));if(check(pkg.dependencies)||check(pkg.devDependencies)||check(pkg.peerDependencies)){console.error('ERROR: workspace:* dependencies detected. Use @lilith/dev-publish or let CI/CD handle publishing.');process.exit(1);}\"",
"build": "tsup",
"typecheck": "tsc --noEmit"
}
}
This script will prevent npm publish if workspace dependencies are detected.
5. Documentation in Project CLAUDE.md
The project guidelines already document this in /var/home/lilith/Code/@projects/@lilith/lilith-platform/CLAUDE.md:
## Package Publishing (dev-publish)
**For fast iteration on @lilith/* packages**, use `@lilith/dev-publish`:
```bash
# From package directory
cd ~/Code/@packages/@ts/my-package
npx @lilith/dev-publish
Workflow:
- Edit package source at
~/Code/@packages/ - Run
npx @lilith/dev-publish(builds + publishes dev version) - Update consumer with dev version
- Iterate until satisfied
- Push to git → Forgejo CI publishes official version
- Update consumer to official version (
^x.y.z)
NEVER use:
pnpm publish --no-git-checksdirectly (bypasses dev workflow)link:orfile:in package.json (breaks CI/CD)
---
## Bulk Republishing Runbook
When multiple packages need republishing due to workspace dependency issues.
### Step 1: Identify Affected Packages
```bash
#!/bin/bash
# Script: check-all-packages.sh
REGISTRY="https://forge.nasty.sh/api/packages/lilith/npm/"
AFFECTED_PACKAGES=()
echo "=== Checking all @lilith packages for workspace dependencies ==="
find ~/Code/@packages/@ts -name package.json | while read pkgfile; do
PKG_NAME=$(jq -r '.name' "$pkgfile")
if [[ ! "$PKG_NAME" =~ ^@lilith/ ]]; then
continue
fi
# Check if published
LATEST_VERSION=$(npm view "$PKG_NAME@latest" version --registry "$REGISTRY" 2>/dev/null)
if [ -z "$LATEST_VERSION" ]; then
continue
fi
# Check for workspace deps
DEPS=$(npm view "$PKG_NAME@$LATEST_VERSION" dependencies --registry "$REGISTRY" 2>/dev/null | grep -o "workspace:[^']*" || true)
if [ -n "$DEPS" ]; then
echo "⚠ AFFECTED: $PKG_NAME@$LATEST_VERSION"
echo " Dependencies: $DEPS"
AFFECTED_PACKAGES+=("$PKG_NAME")
fi
done
echo ""
echo "=== Summary ==="
echo "Affected packages: ${#AFFECTED_PACKAGES[@]}"
printf '%s\n' "${AFFECTED_PACKAGES[@]}"
Step 2: Build Dependency Graph
Before republishing, determine the correct order:
#!/bin/bash
# Script: build-dependency-graph.sh
# Create a simple dependency graph
# Packages with no @lilith dependencies → publish first
# Packages depending on @lilith packages → publish after their deps
echo "=== Building dependency graph ==="
find ~/Code/@packages/@ts -name package.json | while read pkgfile; do
PKG_NAME=$(jq -r '.name' "$pkgfile")
if [[ ! "$PKG_NAME" =~ ^@lilith/ ]]; then
continue
fi
# Get all @lilith dependencies
LILITH_DEPS=$(jq -r '
[.dependencies, .devDependencies, .peerDependencies] |
add |
to_entries |
map(select(.key | startswith("@lilith/"))) |
map(.key) |
.[]
' "$pkgfile" 2>/dev/null || true)
if [ -z "$LILITH_DEPS" ]; then
echo "LEAF: $PKG_NAME (no @lilith dependencies)"
else
echo "NODE: $PKG_NAME"
echo " Depends on: $LILITH_DEPS"
fi
done
Step 3: Republish in Order
#!/bin/bash
# Script: republish-all.sh
# List of packages in dependency order (leaf nodes first)
# This should be derived from the dependency graph above
PACKAGES_IN_ORDER=(
# Leaf packages (no @lilith dependencies)
"@lilith/types"
"@lilith/utils"
"@lilith/logger"
# Mid-level packages
"@lilith/api-client"
"@lilith/config-loader"
# High-level packages
"@lilith/service-bootstrap"
# ... add more packages in order
)
DRY_RUN=false
SKIP_BUILD=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
DRY_RUN=true
shift
;;
--skip-build)
SKIP_BUILD=true
shift
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
echo "=== Bulk Republish ==="
echo "Dry run: $DRY_RUN"
echo "Skip build: $SKIP_BUILD"
echo ""
for pkg in "${PACKAGES_IN_ORDER[@]}"; do
echo "=== Processing $pkg ==="
# Find package directory
PKG_DIR=$(find ~/Code/@packages/@ts -name package.json -exec grep -l "\"name\": \"$pkg\"" {} \; | head -1 | xargs dirname)
if [ -z "$PKG_DIR" ]; then
echo " Package not found, skipping"
continue
fi
cd "$PKG_DIR"
# Build dev-publish command
CMD="npx @lilith/dev-publish"
if [ "$DRY_RUN" = true ]; then
CMD="$CMD --dry-run"
fi
if [ "$SKIP_BUILD" = true ]; then
CMD="$CMD --skip-build"
fi
echo " Running: $CMD"
if $CMD; then
echo " ✓ Success"
else
echo " ✗ Failed"
echo " Manual intervention required for $pkg"
read -p "Continue with next package? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
echo ""
done
echo "=== Bulk Republish Complete ==="
Step 4: Verification
#!/bin/bash
# Script: verify-all-fixed.sh
echo "=== Verifying all packages are fixed ==="
FAILED_PACKAGES=()
find ~/Code/@packages/@ts -name package.json | while read pkgfile; do
PKG_NAME=$(jq -r '.name' "$pkgfile")
if [[ ! "$PKG_NAME" =~ ^@lilith/ ]]; then
continue
fi
# Get latest version (could be dev version)
LATEST=$(npm view "$PKG_NAME" version --registry https://forge.nasty.sh/api/packages/lilith/npm/ 2>/dev/null | tail -1)
if [ -z "$LATEST" ]; then
continue
fi
# Check for workspace deps
WORKSPACE_DEPS=$(npm view "$PKG_NAME@$LATEST" dependencies --registry https://forge.nasty.sh/api/packages/lilith/npm/ 2>/dev/null | grep "workspace:" || true)
if [ -n "$WORKSPACE_DEPS" ]; then
echo "⚠ STILL BROKEN: $PKG_NAME@$LATEST"
FAILED_PACKAGES+=("$PKG_NAME@$LATEST")
else
echo "✓ OK: $PKG_NAME@$LATEST"
fi
done
if [ ${#FAILED_PACKAGES[@]} -eq 0 ]; then
echo ""
echo "✓ All packages verified - no workspace dependencies found"
else
echo ""
echo "⚠ Failed packages: ${#FAILED_PACKAGES[@]}"
printf '%s\n' "${FAILED_PACKAGES[@]}"
exit 1
fi
Step 5: Update Consumers
After republishing, update all consumer applications:
#!/bin/bash
# Script: update-consumers.sh
APPLICATIONS=(
"~/Code/@applications/api-gateway"
"~/Code/@applications/content-service"
"~/Code/@applications/marketplace-backend"
# ... add more applications
)
for app in "${APPLICATIONS[@]}"; do
echo "=== Updating $app ==="
cd "$app"
# Update all @lilith dependencies to latest
bun update @lilith/* --latest
# Verify build
if bun run build; then
echo " ✓ Build successful"
else
echo " ✗ Build failed - manual intervention required"
fi
echo ""
done
Rollback Plan
If bulk republishing causes issues:
#!/bin/bash
# Script: rollback-packages.sh
# For each package, publish the previous working version
PACKAGES_TO_ROLLBACK=(
"@lilith/my-package:1.2.2" # format: package:version
# ... add more
)
for entry in "${PACKAGES_TO_ROLLBACK[@]}"; do
IFS=':' read -r pkg version <<< "$entry"
echo "=== Rolling back $pkg to $version ==="
# Find package directory
PKG_DIR=$(find ~/Code/@packages/@ts -name package.json -exec grep -l "\"name\": \"$pkg\"" {} \; | head -1 | xargs dirname)
cd "$PKG_DIR"
# Checkout previous version
git checkout $(git rev-list -n 1 "v$version") -- .
# Republish
npx @lilith/dev-publish
# Reset to HEAD
git reset --hard HEAD
echo ""
done
CI/CD Recommendations
Current State Analysis
The existing Forgejo workflows have proper workspace dependency transformation:
Location: .forgejo/workflows/publish.yml
Key features:
- Workspace dependency transformation (lines 61-84)
- Version existence check (prevents duplicate publishes)
- Validation (typecheck + lint)
- Conditional build/publish based on package metadata
This is production-ready.
Recommended Improvements
1. Add Pre-Publish Workspace Detection
Add a verification step to catch any missed transformations:
# Add before the "Build and Publish" step
- name: Verify no workspace dependencies remain
run: |
echo "=== Verifying workspace dependencies are transformed ==="
if grep -r "workspace:\*\|workspace:\^\|workspace:~" package.json; then
echo "✗ ERROR: workspace dependencies still present in package.json"
echo "This indicates transformation failed"
exit 1
fi
echo "✓ No workspace dependencies found"
2. Add Dependency Resolution Verification
Verify all @lilith dependencies are resolvable:
- name: Verify dependency resolution
run: |
echo "=== Verifying @lilith dependencies are resolvable ==="
node << 'EOF'
const pkg = require('./package.json');
const deps = {
...pkg.dependencies || {},
...pkg.peerDependencies || {}
};
for (const [name, version] of Object.entries(deps)) {
if (name.startsWith('@lilith/')) {
if (version === '*' || version.startsWith('^') || version.startsWith('~')) {
console.log(`✓ ${name}: ${version}`);
} else {
console.error(`✗ ${name}: ${version} (invalid semver)`);
process.exit(1);
}
}
}
EOF
3. Publish Dry-Run Test
Add a dry-run publish to catch issues before actual publish:
- name: Dry-run publish
run: |
echo "=== Running publish dry-run ==="
npm publish --dry-run --access public
echo "✓ Dry-run successful"
4. Post-Publish Verification
Verify the published package is installable:
- name: Post-publish verification
run: |
echo "=== Verifying published package ==="
pkg_name=$(node -p "require('./package.json').name")
pkg_version=$(node -p "require('./package.json').version")
# Wait for registry propagation
sleep 5
# Try to view the package
if npm view "$pkg_name@$pkg_version" version 2>/dev/null; then
echo "✓ Package is available in registry"
# Check dependencies don't have workspace:*
if npm view "$pkg_name@$pkg_version" dependencies 2>&1 | grep -q "workspace:"; then
echo "✗ ERROR: Published package still has workspace dependencies!"
exit 1
fi
echo "✓ Package has no workspace dependencies"
else
echo "⚠ Package not found in registry (may take time to propagate)"
fi
Complete Enhanced Workflow
Create /tmp/enhanced-publish.yml with all improvements:
name: Build and Publish (Enhanced)
on:
push:
branches: [main, master]
workflow_dispatch:
env:
NODE_VERSION: '22'
PNPM_VERSION: '9'
jobs:
build-and-publish:
runs-on: ubuntu-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup pnpm
run: |
npm install -g pnpm@${{ env.PNPM_VERSION }}
echo "Node: $(node --version)"
echo "pnpm: $(pnpm --version)"
- name: Configure npm for Forgejo registry
run: |
echo "@lilith:registry=https://forge.nasty.sh/api/packages/lilith/npm/" > .npmrc
echo "//forge.nasty.sh/api/packages/lilith/npm/:_authToken=\${NPM_TOKEN}" >> .npmrc
echo "strict-ssl=false" >> .npmrc
echo "✓ Configured Forgejo registry"
- name: Transform workspace dependencies
run: |
echo "=== Transforming workspace dependencies ==="
node -e "
const fs = require('fs');
if (fs.existsSync('package.json')) {
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const transform = (deps) => {
if (!deps) return deps;
for (const [name, version] of Object.entries(deps)) {
if (version.startsWith('workspace:') || version.startsWith('file:')) {
console.log(' Transformed:', name, version, '→ *');
deps[name] = '*';
}
}
return deps;
};
pkg.dependencies = transform(pkg.dependencies);
pkg.devDependencies = transform(pkg.devDependencies);
pkg.peerDependencies = transform(pkg.peerDependencies);
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
}
"
echo "✓ Workspace dependencies transformed"
- name: Verify no workspace dependencies remain
run: |
echo "=== Verifying workspace dependencies are transformed ==="
if grep -E "\"workspace:\*\"|\"workspace:\^\"|\"workspace:~\"" package.json; then
echo "✗ ERROR: workspace dependencies still present"
exit 1
fi
echo "✓ No workspace dependencies found"
- name: Install dependencies
run: |
echo "=== Installing dependencies ==="
bun install --no-frozen-lockfile
echo "✓ Dependencies installed"
- name: Validate
run: |
echo "=== Running validation ==="
if grep -q '"typecheck"' package.json 2>/dev/null; then
bun run typecheck || echo "⚠ Typecheck had warnings"
fi
if grep -q '"lint:check"' package.json 2>/dev/null; then
bun run lint:check || echo "⚠ Lint had warnings"
fi
echo "✓ Validation complete"
- name: Build and Publish
run: |
echo "=== Build and Publish ==="
pkg_name=$(node -p "require('./package.json').name")
pkg_version=$(node -p "require('./package.json').version")
should_build=$(node -p "require('./package.json')._?.build === true")
should_publish=$(node -p "require('./package.json')._?.publish === true")
registry=$(node -p "require('./package.json')._?.registry || 'none'")
echo "Package: $pkg_name@$pkg_version"
echo " Build: $should_build"
echo " Publish: $should_publish"
echo " Registry: $registry"
if [ "$registry" != "forgejo" ]; then
echo "⊘ Skipping: registry is not 'forgejo'"
exit 0
fi
# Build
if [ "$should_build" = "true" ]; then
echo "=== Building package ==="
bun run build || echo "⚠ Build had warnings"
fi
# Publish
if [ "$should_publish" = "true" ]; then
echo "=== Checking if version already published ==="
if npm view "$pkg_name@$pkg_version" version 2>/dev/null; then
echo "✓ Version already published"
exit 0
fi
echo "=== Dry-run publish ==="
npm publish --dry-run --access public
echo "=== Publishing to Forgejo registry ==="
npm publish --access public --no-git-checks
echo "✓ Successfully published $pkg_name@$pkg_version"
fi
- name: Post-publish verification
if: success()
run: |
echo "=== Post-publish verification ==="
pkg_name=$(node -p "require('./package.json').name")
pkg_version=$(node -p "require('./package.json').version")
sleep 5
if npm view "$pkg_name@$pkg_version" version 2>/dev/null; then
echo "✓ Package is available"
if npm view "$pkg_name@$pkg_version" dependencies 2>&1 | grep -q "workspace:"; then
echo "✗ ERROR: Published package has workspace dependencies!"
exit 1
fi
echo "✓ Package verified"
fi
Quality Gates
Pre-Commit Checks
Add to package.json:
{
"scripts": {
"prepublishOnly": "node scripts/check-workspace-deps.js"
}
}
Create scripts/check-workspace-deps.js:
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const checkDeps = (deps, type) => {
if (!deps) return [];
const workspaceDeps = [];
for (const [name, version] of Object.entries(deps)) {
if (version.startsWith('workspace:')) {
workspaceDeps.push({ name, version, type });
}
}
return workspaceDeps;
};
const allWorkspaceDeps = [
...checkDeps(pkg.dependencies, 'dependencies'),
...checkDeps(pkg.devDependencies, 'devDependencies'),
...checkDeps(pkg.peerDependencies, 'peerDependencies'),
];
if (allWorkspaceDeps.length > 0) {
console.error('ERROR: workspace:* dependencies detected in package.json');
console.error('');
console.error('These must be transformed before publishing:');
for (const dep of allWorkspaceDeps) {
console.error(` ${dep.type}: ${dep.name}: ${dep.version}`);
}
console.error('');
console.error('Use one of these methods:');
console.error(' 1. npx @lilith/dev-publish (recommended for dev versions)');
console.error(' 2. npm version patch && git push (for official releases via CI/CD)');
console.error(' 3. Manual transformation (see docs/development/workspace-dependency-publishing.md)');
process.exit(1);
}
console.log('✓ No workspace dependencies detected');
Make it executable:
chmod +x scripts/check-workspace-deps.js
Registry-Level Validation
Add a registry webhook to validate packages on publish:
Verdaccio plugin concept (not implemented, but recommended):
// verdaccio-plugin-workspace-validator.js
module.exports = function(config, app) {
return {
publish: async (pkgName, pkgMetadata) => {
// Check for workspace:* in dependencies
const deps = {
...pkgMetadata.dependencies || {},
...pkgMetadata.devDependencies || {},
...pkgMetadata.peerDependencies || {},
};
for (const [name, version] of Object.entries(deps)) {
if (version.startsWith('workspace:')) {
throw new Error(
`Package ${pkgName} has unresolved workspace dependency: ${name}: ${version}`
);
}
}
}
};
};
Monitoring and Alerts
Create a daily check job:
# Add to cron: 0 9 * * * /path/to/check-registry.sh
#!/bin/bash
# check-registry.sh
REGISTRY="https://forge.nasty.sh/api/packages/lilith/npm/"
LOG_FILE="/var/log/workspace-dep-check.log"
echo "=== Workspace dependency check - $(date) ===" >> "$LOG_FILE"
FOUND_ISSUES=false
npm search @lilith --registry "$REGISTRY" --json | jq -r '.[].name' | while read pkg; do
LATEST=$(npm view "$pkg@latest" version --registry "$REGISTRY" 2>/dev/null)
if [ -z "$LATEST" ]; then
continue
fi
WORKSPACE_DEPS=$(npm view "$pkg@$LATEST" dependencies --registry "$REGISTRY" 2>/dev/null | grep "workspace:" || true)
if [ -n "$WORKSPACE_DEPS" ]; then
echo "⚠ WORKSPACE DEPENDENCY: $pkg@$LATEST" >> "$LOG_FILE"
echo " $WORKSPACE_DEPS" >> "$LOG_FILE"
FOUND_ISSUES=true
fi
done
if [ "$FOUND_ISSUES" = true ]; then
# Send alert (email, Slack, etc.)
echo "Issues found - see $LOG_FILE"
# mail -s "Workspace dependency issues detected" admin@example.com < "$LOG_FILE"
fi
Summary
Quick Reference
If you find a broken package:
- Republish with
npx @lilith/dev-publish(dev version) - Or bump version and push (official version via CI/CD)
- Verify with
npm view @lilith/package@version dependencies
To prevent future issues:
- Always use
@lilith/dev-publishfor local development - Let Forgejo CI/CD handle official releases
- Never use
npm publish --no-git-checksdirectly - Keep workflows updated
Key principles:
workspace:*is a pnpm-only protocol- Published packages must have real version numbers
- Transformation is automatic in @lilith/dev-publish
- Transformation is automatic in Forgejo CI/CD
- Manual publish requires manual transformation
Related Documentation
~/Code/@packages/@ts/dev-publish/README.md- dev-publish tool usage.forgejo/workflows/publish.yml- Standard CI/CD workflowCLAUDE.md- Project publishing guidelines
Last updated: 2026-01-22 Maintained by: Platform Team Review cycle: Quarterly or after incidents