Skip to content
Draft
3 changes: 3 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ SPDX-PackageComment = "The code in this project may include calls to APIs (\"API
path = [
".claude/**",
".github/**",
".opencode/**",
"openspec/**",
"action.yml",
"test-resources/**",
"README.md",
"CONTRIBUTING.md",
"AGENTS.md",
".gitignore",
".husky/**",
"package-lock.json",
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/monorepo-support/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: sdd-plus-superpowers
created: 2026-05-14
59 changes: 59 additions & 0 deletions openspec/changes/monorepo-support/brainstorm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## Design Summary

Add monorepo support to `pull-request-semver-bumper` enabling independent per-package semver bumping within a single repository. The action auto-detects monorepo mode when a `bumper.monorepo.json` config file exists in the target repo root, determines which packages were touched by the PR's changed files, and bumps each one using the same bump level derived from the PR title. The design prioritizes zero impact on existing single-package users by branching early into an isolated code path.

## Alternatives Considered

### Option A: Early Branch — Separate Monorepo Module

- **Approach**: Detect `bumper.monorepo.json` at runtime in `index.ts` after git setup. If present, branch into a completely separate orchestration path (`src/monorepo/orchestrate.ts`) handling config loading, file detection, per-package bumping, and combined commit. Existing single-package path remains untouched.
- **Pros**: Zero risk to existing users; clean separation of concerns; each module independently testable; easy mental model ("if config exists → monorepo path")
- **Cons**: Some shared logic (git setup, PR title parsing) runs before the branch point; two code paths to maintain long-term
- **Why chosen**: See Agreed Approach below

### Option B: Unified Pipeline with Package Loop

- **Approach**: Refactor `index.ts` to always operate on a `Package[]`. Single-package mode produces a one-element list from inputs; monorepo mode produces N entries from config. Same loop handles both.
- **Pros**: DRY, single code path; forces cleaner abstractions
- **Cons**: Touches existing working code (regression risk); complex "N=1 special case"; harder to test parameterized paths; commit message logic gets complicated
- **Why not chosen**: Higher risk to existing users, over-engineers the single-package case

### Option C: Separate Composite Action

- **Approach**: Create a new top-level `action-monorepo.yml` with an entirely separate Node action entry point.
- **Pros**: Complete isolation from existing action
- **Cons**: Massive code duplication; two npm projects to maintain; users must change workflow files; violates "auto-activated, no input changes" requirement
- **Why not chosen**: Breaks the auto-detection requirement and creates maintenance burden

## Agreed Approach

**Option A: Early Branch with isolated `src/monorepo/` modules.**

This was chosen because:
- It preserves backward compatibility by construction — the existing code path is never entered in monorepo mode
- New code is isolated and testable without affecting existing tests
- Shared utilities (git, semver, parse-commit) are reused without refactoring them
- It matches the principle of minimal change to existing working code
- The `type` input defaults to `"auto"` enabling auto-detection for both monorepo and single-package modes

## Key Decisions

- **`type` input defaults to `"auto"`**: When `"auto"` (or omitted), the action auto-detects: `bumper.monorepo.json` exists → monorepo mode, no config → detect single-package type from repo root filesystem markers. Explicit values (`npm`, `maven`, `python`, `version-file`) → existing single-package behavior unchanged.
- **Detection trigger**: `type: "auto"` + `bumper.monorepo.json` exists → monorepo mode. `type: "auto"` + no config → auto-detect single-package type from repo root. Explicit `type` → single-package with that type (unchanged).
- **Shared `detect-type.ts` module**: Same filesystem marker logic (`package.json` → npm, `pom.xml` → maven, `pyproject.toml` → python, `VERSION` → version-file) serves both monorepo per-package detection and single-package auto-detection at repo root.
- **Config lives in target repo**: Each monorepo defines its own structure; the config is read at runtime from the checked-out repo root, after `configureGit()` completes (guarantees PR branch is checked out).
- **Config read timing**: Detection happens after `configureGit()` since repo files aren't guaranteed before that (fallback clone case). Order: read inputs → configureGit → detect mode → branch into monorepo or single-package path. Skip bump-command validation when `type` is `"auto"`.
- **Changed-file detection via git diff**: `git diff --name-only origin/<base>...HEAD` — zero new dependencies, leverages existing `simple-git` and `fetch --all` setup.
- **Config file change triggers full bump**: If `bumper.monorepo.json` itself is among changed files, all declared packages are bumped. Ensures new packages get their initial version.
- **Single bump level, independent versions**: The bump level (major/minor/patch) is derived from the PR title and applied uniformly. But each package maintains its own independent version — e.g., `foo@1.2.0` bumps to `foo@1.3.0` while `bar@3.0.1` bumps to `bar@3.1.0` (both get `minor`). No per-package scope differentiation.
- **Per-package type auto-detection**: Filesystem markers (`package.json` → npm, `pom.xml` → maven, `pyproject.toml` → python, `VERSION` → version-file). Same module used for single-package auto-detection at repo root.
- **Per-package overrides mirror action inputs**: `pom-file`, `version-property-path`, `package-json-file`, `version-file`, `pyproject-file`, `bump-command`, `post-command` — all optional per entry.
- **Single atomic commit**: All package bumps staged together. Format: `chore: bump version packages/foo@1.2.0 packages/bar@2.1.0`. No partial commits on failure.
- **`new-version` output differs by mode**: Single-package → plain string `"1.2.0"`. Monorepo → JSON map `{"packages/foo":"1.2.0","packages/bar":"2.1.0"}`. Consumers adopting monorepo mode adapt their parsing.
- **All-or-nothing error handling**: If any package bump fails, the entire action fails with a clear error identifying the failing package.
- **Package path matching**: A changed file triggers a bump for every declared package whose `path` is a prefix of the file path. Overlapping paths are allowed — file can trigger multiple packages.
- **Glob support in paths**: `"path": "packages/*"` expands to immediate subdirectories (one level only) that contain a detectable version file marker. Expanded packages inherit the parent entry's overrides. Exact paths and glob paths coexist in the same config.

## Open Questions

None — design is fully resolved through brainstorming session.
93 changes: 93 additions & 0 deletions openspec/changes/monorepo-support/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
## Context

The `pull-request-semver-bumper` GitHub Action currently supports single-package repositories only. It reads a PR title (Conventional Commits format), determines the bump level, fetches the current version from the base branch, computes the new version via `semver.inc()`, runs a bump command, and commits the result.

The architecture is linear: `index.ts` → git setup → parse PR → fetch version → bump → commit → push. All version operations target a single file in a single location. There is no concept of multiple packages, scoped commits, or selective bumping.

**Constraints:**
- Backward compatibility is mandatory — existing single-package users must not be affected
- The action is used across SAP repositories; breaking changes require major version bump
- `dist/index.js` is committed (ncc bundle); build must remain simple
- Target repos have heterogeneous structures (npm, maven, python, version-file mixed in one repo)

**Stakeholders:** Monorepo maintainers who currently cannot use this action, plus all existing single-package users who must not experience regressions.

## Goals / Non-Goals

**Goals:**
- Enable per-package semver bumping in monorepos with a single config file
- Auto-detect monorepo mode without requiring changes to existing workflow files
- Support heterogeneous package types within one repo
- Provide glob-based package discovery for convenience
- Maintain zero impact on existing single-package behavior

**Non-Goals:**
- Per-package bump levels based on commit scopes (e.g., `feat(auth):`) — single bump level for all touched packages
- Synchronized versioning (all packages share one version number) — each package maintains its own independent version
- Recursive glob discovery (`**`) — only one level deep (`*`)
- Independent versioning strategies per package (e.g., one on pre-release, another on stable)
- Single-package type auto-detection is included in this change (not deferred) — shared `detect-type.ts` module
- Workspace dependency graph resolution (e.g., bumping dependents when a dependency bumps)

## Decisions

**Decision: Early branch architecture**
- Chosen: Detect monorepo config after `configureGit()`, branch into isolated `src/monorepo/` modules
- Reason: Zero risk to existing users by construction — the single-package code path is never entered in monorepo mode. New code is independently testable.
- Alternatives considered: Unified pipeline (regression risk, over-engineering), separate composite action (code duplication, breaks auto-detection requirement)

**Decision: `type` input defaults to `"auto"`**
- Chosen: Default value `"auto"` triggers auto-detection. Config file presence determines monorepo vs single-package. Same `detect-type.ts` module handles both cases.
- Reason: Explicit default value is clearer than empty-string semantics. Existing users with explicit types are unaffected. New users get auto-detection out of the box. Single `"auto"` value cleanly covers both monorepo detection and single-package type detection.
- Alternatives considered: No default / empty string (less explicit), required input (blocks auto-detection), separate boolean input (unnecessary given config file detection)

**Decision: Git diff for changed-file detection**
- Chosen: `git diff --name-only origin/<base>...HEAD` via existing `simple-git`
- Reason: Zero new dependencies. `configureGit()` already runs `fetch --all`, so base branch ref is available. Three-dot diff gives exactly "changes introduced by PR branch." No pagination limits.
- Alternatives considered: GitHub API `pulls/{pr}/files` (requires Octokit, pagination handling for >100 files, stale `@octokit/rest@17.x` already in deps)

**Decision: Config read after `configureGit()`**
- Chosen: Read `bumper.monorepo.json` only after git setup completes
- Reason: The action has a fallback clone path (if `.git` doesn't exist). Repo files aren't guaranteed on disk until after `configureGit()`. Reading after checkout also ensures we get the PR branch's version of the config.
- Alternatives considered: Read before git setup (breaks in fallback clone case)

**Decision: Glob expansion at config load time (one level only)**
- Chosen: `"path": "packages/*"` expands to immediate subdirectories containing a version file marker
- Reason: Convenience for large monorepos without recursive discovery surprises. `*` naturally means one level in glob semantics. `**` reserved for future use (rejected today).
- Alternatives considered: Require all paths to be explicit (tedious for 10+ packages), recursive discovery (too risky, catches test fixtures)

**Decision: All-match semantics for path overlap**
- Chosen: A changed file triggers bumps for ALL packages whose `path` is a prefix match
- Reason: Simpler than "first match wins" (no ordering dependency). Config owner controls overlap by choosing non-overlapping paths if desired.
- Alternatives considered: First match wins (config order matters — fragile), longest prefix match (complex, unnecessary)

**Decision: `new-version` output format differs by mode**
- Chosen: Plain string in single-package mode, JSON map in monorepo mode
- Reason: Monorepo mode is new — consumers opting in already make workflow changes. Clean semantic: mode determines output format.
- Alternatives considered: Always JSON (breaks existing consumers), dynamic per-package outputs (fragile naming)

## Risks / Trade-offs

- [Two code paths to maintain long-term] → Shared utilities (git, semver, parse-commit) remain DRY; only orchestration logic is duplicated. Acceptable given backward compat guarantee.
- [Glob expansion could discover unintended packages] → Mitigated by requiring a version file marker in subdirectory. Directories without `package.json`/`pom.xml`/`pyproject.toml`/`VERSION` are skipped.
- [`type: "auto"` with no config and no detectable type at repo root] → Clear error: "Could not detect package type. Specify `type` explicitly or add a version file (package.json, pom.xml, pyproject.toml, or VERSION) to the repo root."
- [Large PRs with many changed files] → Git diff has no pagination limits (unlike GitHub API's 3000 file cap). Performance is bounded by git operation time, which is already the baseline.
- [Config file change triggers bump of ALL packages] → Deliberate — ensures new packages get initial version. Documented behavior; users can split config changes into separate PRs if needed.

## Migration Plan

**Deployment steps:**
1. Make `type` input optional with default `"auto"` in root `action.yml` and all composite wrappers
2. Add `detect-type.ts` module with shared filesystem marker detection logic
3. Ship new `src/monorepo/` modules behind config file detection gate
4. Single-package auto-detection enabled when `type` is `"auto"` and no monorepo config exists
5. Existing users: no change needed — their explicit `type: npm/maven/etc.` bypasses all auto-detection
6. New monorepo users: add `bumper.monorepo.json` to repo root, leave `type` at default `"auto"`
7. New single-package users: can leave `type` at default `"auto"` if repo root has a detectable version file

**Rollback:**
- Revert to previous version tag (`@v1` → `@v1` prior release). Since monorepo mode is additive and gated behind config file detection, there is no state to clean up — removing the config file also disables monorepo mode.

## Open Questions

None — all design decisions resolved during brainstorming.
48 changes: 48 additions & 0 deletions openspec/changes/monorepo-support/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## Why

Monorepo repositories cannot use `pull-request-semver-bumper` today because the action only supports a single version file in a single location. Teams with multiple independently-versioned packages in one repo must either maintain separate CI jobs with hardcoded paths or forgo automated version bumping entirely. Adding monorepo support and type auto-detection removes this gap, making the action usable for the growing number of SAP monorepos while simultaneously simplifying onboarding for new single-package users who no longer need to specify their build type explicitly.

## What Changes

**`type` Input Behavior**
- From: `type` is required; must be one of `npm`, `maven`, `python`, `version-file`
- To: `type` defaults to `"auto"`; auto-detects monorepo mode (via config file) or single-package type (via filesystem markers). Explicit values still work unchanged.
- Reason: Enables monorepo auto-activation and simplifies single-package onboarding
- Impact: Non-breaking — existing users pass explicit types which bypass auto-detection

**New Monorepo Orchestration Path**
- From: Single linear pipeline in `index.ts` (one package, one bump, one commit)
- To: Early branch after `configureGit()` into isolated `src/monorepo/` modules when `bumper.monorepo.json` is detected
- Reason: Zero risk to existing single-package users; clean separation of concerns
- Impact: Non-breaking — additive code path gated behind config file presence

**`new-version` Output Format**
- From: Always a plain string (`"1.2.0"`)
- To: Plain string in single-package mode; JSON map (`{"packages/foo":"1.2.0","packages/bar":"2.1.0"}`) in monorepo mode
- Reason: Monorepo mode produces multiple versions; a map is the natural representation
- Impact: Non-breaking — only monorepo mode (new feature) produces the map format

## Capabilities

### New Capabilities

- `monorepo-config-loading`: Parse and validate `bumper.monorepo.json` from target repo root, expand glob patterns (`packages/*`) one level deep, auto-detect package types from filesystem markers
- `changed-file-detection`: Cross-reference PR changed files (via `git diff`) against declared package paths to determine which packages need bumping
- `monorepo-orchestration`: Coordinate per-package version bumping — fetch current version per package from base branch, apply uniform bump level, execute per-package bump/post commands, produce single atomic commit
- `type-auto-detection`: Shared module detecting package type from filesystem markers (`package.json` → npm, `pom.xml` → maven, `pyproject.toml` → python, `VERSION` → version-file). Serves both monorepo per-package detection and single-package auto-detection at repo root.
- `e2e-test-fixtures`: Restructure `test-resources/` as a single monorepo-style layout with per-type packages. Monorepo E2E tests use the config directly; single-package E2E tests map paths to individual package directories. One fixture set serves both modes.

### Modified Capabilities

None — existing single-package behavior is preserved unchanged. The `type` input default change is additive (new default value, existing explicit values unaffected).

## Impact

- **Source code**: New `src/monorepo/` directory (4 modules); minor addition to `index.ts` (early branch); extension to `git/git.ts` (changed-file detection, multi-package commit message)
- **action.yml**: `type` input changes from required to optional with default `"auto"`
- **Composite wrappers**: Unaffected (they pass explicit `build-type` to core action)
- **Dependencies**: None added — uses existing `simple-git` and `semver`
- **Outputs**: `new-version` format differs in monorepo mode (JSON map vs string)
- **CI**: Existing tests restructured; single-package E2E tests point to individual package paths within the unified fixture. New monorepo E2E tests added.
- **Test resources**: `test-resources/` reorganized as monorepo-style layout — single fixture set serves both monorepo and single-package E2E tests
- **Bundle size**: Modest increase in `dist/index.js` from new modules
Loading
Loading