Skip to content

Commit 3dc2a98

Browse files
committed
Refactor CI workflows to streamline pre-release checks and synchronize package versions
1 parent e80d062 commit 3dc2a98

File tree

7 files changed

+198
-45
lines changed

7 files changed

+198
-45
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,8 @@ jobs:
2424
- name: Install dependencies
2525
run: pnpm install --frozen-lockfile
2626

27-
- name: Build
28-
run: pnpm build
29-
30-
- name: Validate templates
31-
run: pnpm validate:templates
32-
33-
- name: Type check
34-
run: pnpm typecheck
35-
36-
- name: Run tests
37-
run: pnpm test
27+
- name: Run pre-release checks
28+
run: pnpm pre-release
3829

3930
- name: Generate coverage
4031
run: pnpm test:coverage

.github/workflows/publish-dev.yml

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ jobs:
2727

2828
- name: Auto-bump dev version
2929
run: |
30-
# Get current version from CLI package
31-
CURRENT_VERSION=$(node -p "require('./packages/cli/package.json').version")
30+
# Get current version from root package.json (single source of truth)
31+
CURRENT_VERSION=$(node -p "require('./package.json').version")
3232
3333
# Extract base version (remove any prerelease suffix)
3434
BASE_VERSION=$(echo $CURRENT_VERSION | sed 's/-dev\..*//')
@@ -39,25 +39,22 @@ jobs:
3939
4040
echo "Bumping version to: $DEV_VERSION"
4141
42-
# Update all package.json files
42+
# Update root package.json only
4343
node -e "
4444
const fs = require('fs');
45-
const packages = ['packages/cli', 'packages/ui', 'packages/mcp', '.'];
46-
packages.forEach(pkg => {
47-
const path = \`\${pkg}/package.json\`;
48-
const json = JSON.parse(fs.readFileSync(path, 'utf8'));
49-
json.version = '$DEV_VERSION';
50-
fs.writeFileSync(path, JSON.stringify(json, null, 2) + '\n');
51-
});
45+
const path = 'package.json';
46+
const json = JSON.parse(fs.readFileSync(path, 'utf8'));
47+
json.version = '$DEV_VERSION';
48+
fs.writeFileSync(path, JSON.stringify(json, null, 2) + '\n');
5249
"
5350
51+
# Sync all workspace packages using our version sync script
52+
pnpm sync-versions
53+
5454
echo "DEV_VERSION=$DEV_VERSION" >> $GITHUB_ENV
5555
56-
- name: Type check
57-
run: pnpm typecheck
58-
59-
- name: Build
60-
run: pnpm build
56+
- name: Run pre-release checks
57+
run: pnpm pre-release
6158

6259
- name: Publish CLI to npm with dev tag
6360
working-directory: packages/cli

.github/workflows/publish.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,8 @@ jobs:
2424
- name: Install dependencies
2525
run: pnpm install --frozen-lockfile
2626

27-
- name: Type check
28-
run: pnpm typecheck
29-
30-
- name: Build
31-
run: pnpm build
27+
- name: Run pre-release checks
28+
run: pnpm pre-release
3229

3330
- name: Publish CLI to npm
3431
working-directory: packages/cli

CONTRIBUTING.md

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,69 @@ pnpm typecheck # Type check all packages (with caching)
3333

3434
## Version Management
3535

36-
All packages in the monorepo should maintain synchronized versions for consistency:
36+
All packages in the monorepo maintain synchronized versions automatically. The root `package.json` serves as the single source of truth.
3737

38+
**Packages:**
3839
- `lean-spec` (CLI package)
3940
- `@leanspec/core` (shared core library)
4041
- `@leanspec/ui` (web UI package)
4142
- `@leanspec/mcp` (MCP server wrapper)
4243

43-
**Before Publishing:**
44-
1. Update version in all `package.json` files (root and all packages)
45-
2. Update cross-package dependencies (e.g., `@leanspec/mcp` depends on `lean-spec`)
46-
3. Run `pnpm build` to verify all packages build successfully
47-
4. Run `node bin/lean-spec.js validate` to check specs
48-
5. Test package installation locally using `npm pack`
44+
### Automated Version Sync
45+
46+
The `pnpm sync-versions` script automatically synchronizes all package versions with the root:
47+
48+
```bash
49+
# Check current version alignment (dry run)
50+
pnpm sync-versions --dry-run
51+
52+
# Sync all package versions to match root package.json
53+
pnpm sync-versions
54+
```
55+
56+
The script:
57+
- Reads the version from root `package.json`
58+
- Updates all workspace packages to match
59+
- Reports what changed
60+
- Runs automatically as part of `pre-release`
4961

50-
**Example version bump:**
62+
### Release Process
63+
64+
**Before Publishing:**
65+
1. Update version in **root `package.json` only**
66+
2. Run `pnpm sync-versions` (or it runs automatically with `pre-release`)
67+
3. Update cross-package dependencies if needed (e.g., `@leanspec/mcp``lean-spec`)
68+
4. Run `pnpm build` to verify all packages build successfully
69+
5. Run `pnpm pre-release` to run full validation suite
70+
- Includes: sync-versions, typecheck, tests, build, and validate with `--warnings-only`
71+
- The validate step treats all issues as warnings (won't fail on complexity/token issues)
72+
- For stricter validation before committing spec changes, run `node bin/lean-spec.js validate` without flags
73+
6. Test package installation locally using `npm pack`
74+
75+
**Version Bump Example:**
5176
```bash
52-
# Bump all packages from 0.2.4 to 0.2.5
53-
# Update: package.json, packages/cli/package.json, packages/core/package.json,
54-
# packages/ui/package.json, packages/mcp/package.json
55-
# Also update: packages/mcp/package.json dependency on lean-spec
77+
# 1. Update root version
78+
npm version patch # or minor/major
79+
80+
# 2. Sync all packages (automatic in pre-release)
81+
pnpm sync-versions
82+
83+
# 3. Verify
84+
pnpm build
85+
pnpm test:run
86+
87+
# 4. Commit and publish
88+
git add .
89+
git commit -m "chore: release v0.2.6"
90+
git push
5691
```
5792

93+
**Why root as source of truth?**
94+
- Single place to update version
95+
- Prevents version drift
96+
- Automated sync in CI/CD
97+
- Simpler release process
98+
5899
### Docs Site Submodule
59100

60101
The docs are maintained in [codervisor/lean-spec-docs](https://github.com/codervisor/lean-spec-docs) and pulled in via the `docs-site/` git submodule. Typical workflow:

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"build": "pnpm build:templates && turbo run build",
1010
"build:templates": "tsx scripts/build-agents-templates.ts",
1111
"validate:templates": "tsx scripts/validate-agents-templates.ts",
12+
"sync-versions": "tsx scripts/sync-versions.ts",
1213
"dev": "turbo run dev --filter=@leanspec/ui --filter=@leanspec/core",
1314
"dev:web": "turbo run dev --filter=@leanspec/ui",
1415
"dev:cli": "turbo run dev --filter=lean-spec",
@@ -18,7 +19,7 @@
1819
"test:ui": "vitest --ui",
1920
"test:run": "vitest run",
2021
"test:coverage": "vitest run --coverage",
21-
"pre-release": "pnpm typecheck && pnpm test:run && pnpm build && node bin/lean-spec.js validate",
22+
"pre-release": "pnpm sync-versions && pnpm typecheck && pnpm test:run && pnpm build && node bin/lean-spec.js validate --warnings-only",
2223
"docs:dev": "pnpm --dir docs-site start",
2324
"docs:build": "pnpm --dir docs-site build",
2425
"docs:serve": "pnpm --dir docs-site serve"

packages/cli/src/commands/validate.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface ValidateOptions {
3131
quiet?: boolean; // Suppress warnings, only show errors
3232
format?: 'default' | 'json' | 'compact'; // Output format
3333
rule?: string; // Filter by specific rule name
34+
warningsOnly?: boolean; // Treat all issues as warnings, never fail (useful for CI)
3435
}
3536

3637
interface ValidationResultWithSpec {
@@ -52,6 +53,7 @@ export function validateCommand(): Command {
5253
.option('--quiet', 'Suppress warnings, only show errors')
5354
.option('--format <format>', 'Output format: default, json, compact', 'default')
5455
.option('--rule <rule>', 'Filter by specific rule name (e.g., max-lines, frontmatter)')
56+
.option('--warnings-only', 'Treat all issues as warnings, never fail (useful for CI pre-release checks)')
5557
.action(async (specs: string[] | undefined, options: ValidateOptions) => {
5658
const passed = await validateSpecs({
5759
maxLines: options.maxLines,
@@ -60,6 +62,7 @@ export function validateCommand(): Command {
6062
quiet: options.quiet,
6163
format: options.format,
6264
rule: options.rule,
65+
warningsOnly: options.warningsOnly,
6366
});
6467
process.exit(passed ? 0 : 1);
6568
});
@@ -151,7 +154,13 @@ export async function validateSpecs(options: ValidateOptions = {}): Promise<bool
151154
const output = formatValidationResults(results, specs, config.specsDir, formatOptions);
152155
console.log(output);
153156

154-
// Determine if validation passed (any errors = failed)
157+
// Determine if validation passed
158+
if (options.warningsOnly) {
159+
// In warnings-only mode, always pass (just report issues)
160+
return true;
161+
}
162+
163+
// Normal mode: any errors = failed
155164
const hasErrors = results.some(r => !r.result.passed);
156165
return !hasErrors;
157166
}

scripts/sync-versions.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env tsx
2+
/**
3+
* Sync versions across workspace packages
4+
*
5+
* This script ensures all workspace packages use the same version as the root package.json.
6+
* It reads the version from the root package.json and updates all packages in the monorepo.
7+
*
8+
* Usage:
9+
* pnpm sync-versions [--dry-run]
10+
*/
11+
12+
import fs from 'node:fs/promises';
13+
import path from 'node:path';
14+
import { fileURLToPath } from 'node:url';
15+
16+
const __filename = fileURLToPath(import.meta.url);
17+
const __dirname = path.dirname(__filename);
18+
19+
const ROOT_DIR = path.resolve(__dirname, '..');
20+
const PACKAGES_DIR = path.join(ROOT_DIR, 'packages');
21+
22+
interface PackageJson {
23+
name: string;
24+
version: string;
25+
[key: string]: any;
26+
}
27+
28+
async function readJsonFile(filePath: string): Promise<PackageJson> {
29+
const content = await fs.readFile(filePath, 'utf-8');
30+
return JSON.parse(content);
31+
}
32+
33+
async function writeJsonFile(filePath: string, data: PackageJson): Promise<void> {
34+
const content = JSON.stringify(data, null, 2) + '\n';
35+
await fs.writeFile(filePath, content, 'utf-8');
36+
}
37+
38+
async function getPackageDirs(): Promise<string[]> {
39+
const entries = await fs.readdir(PACKAGES_DIR, { withFileTypes: true });
40+
return entries
41+
.filter(entry => entry.isDirectory())
42+
.map(entry => path.join(PACKAGES_DIR, entry.name));
43+
}
44+
45+
async function syncVersions(dryRun: boolean = false): Promise<void> {
46+
console.log('🔄 Syncing workspace package versions...\n');
47+
48+
// Read root package.json version
49+
const rootPackageJsonPath = path.join(ROOT_DIR, 'package.json');
50+
const rootPackage = await readJsonFile(rootPackageJsonPath);
51+
const targetVersion = rootPackage.version;
52+
53+
console.log(`📦 Root version: ${targetVersion}\n`);
54+
55+
// Get all package directories
56+
const packageDirs = await getPackageDirs();
57+
58+
let updated = 0;
59+
let skipped = 0;
60+
let errors = 0;
61+
62+
for (const packageDir of packageDirs) {
63+
const packageJsonPath = path.join(packageDir, 'package.json');
64+
65+
try {
66+
const pkg = await readJsonFile(packageJsonPath);
67+
const packageName = pkg.name;
68+
const currentVersion = pkg.version;
69+
70+
if (currentVersion === targetVersion) {
71+
console.log(`✓ ${packageName}: ${currentVersion} (already synced)`);
72+
skipped++;
73+
} else {
74+
console.log(`⚠ ${packageName}: ${currentVersion}${targetVersion}`);
75+
76+
if (!dryRun) {
77+
pkg.version = targetVersion;
78+
await writeJsonFile(packageJsonPath, pkg);
79+
console.log(` ✓ Updated`);
80+
} else {
81+
console.log(` ℹ Would update (dry run)`);
82+
}
83+
updated++;
84+
}
85+
} catch (error) {
86+
console.error(`✗ Error processing ${path.basename(packageDir)}:`, error);
87+
errors++;
88+
}
89+
}
90+
91+
console.log(`\n${'='.repeat(50)}`);
92+
console.log(`Summary:`);
93+
console.log(` Updated: ${updated}`);
94+
console.log(` Already synced: ${skipped}`);
95+
console.log(` Errors: ${errors}`);
96+
97+
if (dryRun && updated > 0) {
98+
console.log(`\n💡 Run without --dry-run to apply changes`);
99+
} else if (!dryRun && updated > 0) {
100+
console.log(`\n✅ Version sync complete!`);
101+
} else if (updated === 0 && errors === 0) {
102+
console.log(`\n✅ All packages already in sync!`);
103+
}
104+
105+
if (errors > 0) {
106+
process.exit(1);
107+
}
108+
}
109+
110+
// Parse CLI args
111+
const args = process.argv.slice(2);
112+
const dryRun = args.includes('--dry-run');
113+
114+
syncVersions(dryRun).catch(error => {
115+
console.error('Fatal error:', error);
116+
process.exit(1);
117+
});

0 commit comments

Comments
 (0)