diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f712ef6d..b621f327 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,13 +93,16 @@ ci(github): update workflow **Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `build`, `revert` **Scope rules**: -- **`feat`, `fix`, or breaking changes** (`!` marker or `BREAKING CHANGE` footer): Must use full Nx project name +- **`feat` or breaking changes** (`!` marker or `BREAKING CHANGE` footer): Must use full Nx project name - Valid: `feat(@redhat-cloud-services/rbac-client): ...` - Invalid: `feat(rbac): ...` or `feat(deps): ...` +- **`fix`**: Any scope or no scope allowed + - Valid: `fix(@redhat-cloud-services/rbac-client): ...`, `fix(deps): ...`, `fix: ...` + - With `useCommitScope: true`, all `fix` commits result in patch bumps regardless of scope - **Non-versioning types** (`chore`, `docs`, `ci`, etc.): Can use short names or area scopes (`deps`, `ci`, `readme`) - Valid: `chore(deps): ...` or `docs(readme): ...` -**Why full names for versioning commits?** Nx uses commit scope to determine which package gets bumped. Short scopes or area scopes cause Nx to silently cap version bumps at patch, even when `feat` or breaking change promises minor/major. +**Why full names for `feat`/breaking?** Nx uses commit scope to determine bump magnitude. For `feat`, scoped projects get minor, non-scoped file-affected projects get capped at patch. For `fix`, all projects always get patch regardless of scope, so full names are optional. ## Testing @@ -118,6 +121,27 @@ See [docs/testing-guidelines.md](docs/testing-guidelines.md) for details on test - Include integration tests for new client packages or endpoint changes - PR descriptions should reference the Jira ticket if applicable +### Dependency Updates (Renovate) + +Renovate is configured to automatically create PRs for dependency updates using `fix(deps):` commit format for production dependencies and `chore(deps-dev):` for dev dependencies. + +**Production dependencies** (touches `packages//package.json`): +- Renovate creates PR with title: `fix(deps): bump axios from 1.17.0 to 1.18.0` +- Merge as-is +- Nx Release bumps all file-affected workspace packages (patch) + +**Dev dependencies** (only root `package.json` changed): +- Renovate creates PR with title: `chore(deps-dev): bump webpack from 5.0.0 to 5.1.0` +- Merge as-is +- No version bumps (chore type has `semverBump: 'none'`) + +**Why this works:** +1. Renovate configured in `renovate.json` with `commitMessagePrefix: "fix(deps):"` for production deps +2. Nx uses two-phase versioning with `useCommitScope: true`: + - **File detection**: Always finds all projects with modified files (same as `nx affected`) + - **Bump magnitude**: `fix` type results in patch for all file-affected packages +3. Security patches are released immediately instead of waiting for the next unrelated feature commit + ## Release Process Releases are automated via Nx Release on the `main` branch. Each package is independently versioned based on conventional commit history. Do not manually bump versions. diff --git a/commitlint.config.js b/commitlint.config.js index bcca18e8..9367837b 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -37,9 +37,9 @@ module.exports = { const isBreaking = !!(header && header.includes('!:')) || !!(notes && notes.some((n) => n.title === 'BREAKING CHANGE')); - const isVersioningType = ['feat', 'fix'].includes(type); + const requiresFullScope = type === 'feat' || isBreaking; - if (!isBreaking && !isVersioningType) { + if (!requiresFullScope) { return [true]; } @@ -54,7 +54,7 @@ module.exports = { } if (!scope) { - const commitType = isBreaking ? 'breaking (!)/feat/fix' : 'feat/fix'; + const commitType = isBreaking ? 'breaking (!)/feat' : 'feat'; return [ false, `Scope required for ${commitType} commits.\n` + @@ -67,9 +67,7 @@ module.exports = { for (const s of scopes) { if (!projectSet.has(s)) { - const commitType = isBreaking - ? 'breaking (!)/feat/fix' - : 'feat/fix'; + const commitType = isBreaking ? 'breaking (!)/feat' : 'feat'; const hint = s.startsWith('@redhat-cloud-services/') ? `Project "${s}" not found. Run: npx nx show projects` : `Use full project name like: ${type}(@redhat-cloud-services/rbac-client)${isBreaking ? '!' : ''}: ...`; diff --git a/commitlint.config.test.js b/commitlint.config.test.js index d534663a..d5c3c265 100644 --- a/commitlint.config.test.js +++ b/commitlint.config.test.js @@ -93,6 +93,26 @@ describe('commitlint scope-full-name-for-versioning', () => { ); expect(result.valid).toBe(true); }); + + it('fix with short scope (allowed)', async () => { + const result = await lintMessage('fix(scheduler): handle error'); + expect(result.valid).toBe(true); + }); + + it('fix with no scope (allowed)', async () => { + const result = await lintMessage('fix: global change'); + expect(result.valid).toBe(true); + }); + + it('fix with deps scope (dependabot)', async () => { + const result = await lintMessage('fix(deps): bump axios to 1.18.0'); + expect(result.valid).toBe(true); + }); + + it('fix with deps-dev scope (dependabot)', async () => { + const result = await lintMessage('fix(deps-dev): bump webpack to 5.1.0'); + expect(result.valid).toBe(true); + }); }); describe('invalid commits', () => { @@ -105,10 +125,6 @@ describe('commitlint scope-full-name-for-versioning', () => { expect(result.errors[0].message).toContain('Use full project name like'); }); - it('fix with short scope', async () => { - const result = await lintMessage('fix(scheduler): handle error'); - expect(result.valid).toBe(false); - }); it('breaking chore with short scope (! marker)', async () => { const result = await lintMessage('chore(rbac)!: breaking change'); @@ -149,11 +165,6 @@ describe('commitlint scope-full-name-for-versioning', () => { expect(result.errors[0].message).toContain('Scope required'); }); - it('fix with no scope', async () => { - const result = await lintMessage('fix: global change'); - expect(result.valid).toBe(false); - expect(result.errors[0].message).toContain('Scope required'); - }); it('breaking with no scope', async () => { const result = await lintMessage('fix!: breaking fix'); @@ -177,12 +188,5 @@ describe('commitlint scope-full-name-for-versioning', () => { ); }); - it('fix with area scope (docs)', async () => { - const result = await lintMessage('fix(docs): update readme'); - expect(result.valid).toBe(false); - expect(result.errors[0].message).toContain( - 'must be a full Nx project name' - ); - }); }); }); diff --git a/renovate.json b/renovate.json index 1e22bb64..7ceb3f94 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,11 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>RedHatInsights/shared-workflows//renovate/frontend"] + "extends": ["github>RedHatInsights/shared-workflows//renovate/frontend"], + "commitMessagePrefix": "fix(deps):", + "packageRules": [ + { + "matchDepTypes": ["devDependencies"], + "commitMessagePrefix": "chore(deps-dev):" + } + ] }