Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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/<name>/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.
Expand Down
10 changes: 4 additions & 6 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand All @@ -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` +
Expand All @@ -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 ? '!' : ''}: ...`;
Expand Down
36 changes: 20 additions & 16 deletions commitlint.config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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'
);
});
});
});
9 changes: 8 additions & 1 deletion renovate.json
Original file line number Diff line number Diff line change
@@ -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):"
}
]
}
Loading