# Release Management

> Orchestrates the complete release preparation process - detects changed products, analyzes commits for version bumps, updates version.json files, generates manifests and changelogs, and creates release branches. Use when preparing a new release.

- Skill: `umbraco/release-management-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add umbraco/release-management-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/umbraco/release-management-2/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: umbraco (https://skillmd.com/u/umbraco)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/umbraco/release-management-2

---


# Release Manager

You are the orchestrator for preparing releases in the Umbraco.Automate repository.

## Task

Guide users through the complete release preparation process:

1. **Detect changed products** since their last release tags
2. **Analyze commits** to recommend version bumps (major/minor/patch)
3. **Cascade forced bumps** to every dependent of a product undergoing minor/major/downplayed-breaking changes
4. **Confirm versions** with the user (including cascaded products)
5. **Update Directory.Packages.props** inter-product dependency ranges (with user approval)
6. **Create release branch** (e.g., `v18/release/2026.02.1`) and switch to it
7. **Dependency validation** - Check for cross-product conflicts using the graph built in cascade analysis
8. **Update version.json** files for each product
9. **Generate release-manifest.json** via `/release-manifest-management`
10. **Generate CHANGELOG.md** files via `/changelog-management`
11. **Changelog review** - Review generated changelogs for quality and completeness
12. **Validate** all files are consistent
13. **Commit all changes** to the release branch

## Workflow

### Phase 1: Change Detection

1. **Find all products** - Discover Umbraco.Automate* directories at repo root

2. **For each product**, detect changes since last release:
    ```bash
    # Find the most recent release tag for this product that is REACHABLE from HEAD.
    # Use git describe (reachability-based), NOT `git tag --list --sort=-version:refname`
    # (version-sorted). On parallel support lines (e.g. support/17.x alongside an 18.x
    # line on dev/main), a version sort would pick the highest tag across ALL branches —
    # so a 17.x release run would wrongly grab an 18.x tag as its base. git describe walks
    # back from HEAD and only sees tags on this line's history. This matches CI's
    # detect-changes.ps1 (`git describe --tags --abbrev=0 --match=...`), so the skill and
    # CI agree on each product's base tag.
    git describe --tags --abbrev=0 --match="<Product>@*"   # e.g. --match="Umbraco.Automate@*"
    # Exit code != 0 means no matching tag is reachable (first release on this line) —
    # fall back to merge-base with this line's vN/main (see Error Handling).

    # Get commits affecting this product since that tag
    git log <tag>..HEAD --oneline -- <ProductFolder>/

    # Exclude non-substantive changes (CHANGELOG.md, version.json)
    git diff <tag>..HEAD --name-only -- <ProductFolder>/ | grep -v 'CHANGELOG.md\|version.json'
    ```

3. **Present changed products** to user:
    ```
    Detected changes since last release:

    ┌───────────────────────────────┬──────────┬─────────────────────────────┐
    │ Product                       │ Last Tag │ Changes                     │
    ├───────────────────────────────┼──────────┼─────────────────────────────┤
    │ Umbraco.Automate              │ 0.1.0    │ 12 commits (3 feat, 2 fix)  │
    │ Umbraco.Automate.OpenIddict   │ 0.1.0    │ 3 commits (1 fix)           │
    │ Umbraco.Automate.Slack        │ 0.1.0    │ 5 commits (1 BREAKING)      │
    └───────────────────────────────┴──────────┴─────────────────────────────┘
    ```

### Phase 2: Version Bump Analysis

For each changed product:

1. **Analyze commit types** since last tag:
    ```bash
    # Get all commits with their messages
    git log <tag>..HEAD --pretty=format:"%s" -- <ProductFolder>/
    ```

2. **Determine bump level** based on conventional commits:
    - **BREAKING CHANGE** in body or exclamation mark after scope → **Major** (1.0.0 → 2.0.0)
    - feat: or feat(scope): → **Minor** (1.0.0 → 1.1.0)
    - fix:, perf: → **Patch** (1.0.0 → 1.0.1)
    - Only docs:, chore:, refactor: → **No bump** (but user can override)

3. **Read current version** from `<Product>/version.json`

4. **Calculate new version**:
    - Major: Increment X in X.Y.Z, reset Y and Z to 0
    - Minor: Increment Y in X.Y.Z, reset Z to 0
    - Patch: Increment Z in X.Y.Z

5. **Present recommendations**:
    ```
    Version bump recommendations:

    ┌───────────────────────────────┬──────────┬──────────┬─────────────────────────────┐
    │ Product                       │ Current  │ Proposed │ Reason                      │
    ├───────────────────────────────┼──────────┼──────────┼─────────────────────────────┤
    │ Umbraco.Automate              │ 0.1.0    │ 0.2.0    │ 3 feat, 2 fix commits       │
    │ Umbraco.Automate.OpenIddict   │ 0.1.0    │ 0.1.1    │ 1 fix commit                │
    │ Umbraco.Automate.Slack        │ 0.1.0    │ 1.0.0    │ 1 BREAKING CHANGE           │
    └───────────────────────────────┴──────────┴──────────┴─────────────────────────────┘
    ```

### Phase 2.5: Dependency Cascade Analysis

After computing direct version bumps in Phase 2, walk the inter-product dependency graph and **force-add** every product that depends on a product undergoing a `feat:` (minor), downplayed-breaking, or major bump. This guarantees the new lower-bound dep range in `Directory.Packages.props` propagates to all consumers in the same release, closing the gap where an old dependent already on NuGet has a stale range that can resolve to the new upstream.

#### Why this matters

NuGet does not surface "feature added in upstream minor" semantics. If `Umbraco.Automate` 0.2.x → 0.3.0 adds a new API and `Umbraco.Automate.Slack` starts using it, a Slack build that still declares `Umbraco.Automate.Core [0.2.0, 0.999.999)` can resolve to Core 0.2.x and crash with `MissingMethodException`. Cascading a forced patch bump on every dependent ensures their new `.nuspec` minimum points at the new upstream, so any user upgrading through the dependent automatically gets the matching upstream.

The downplayed-breaking option in Phase 3 only remains safe because of this cascade — without it, "downplayed breaking" silently breaks consumers stuck on old dependent versions.

#### Step 1 — Build the package → product map (dynamically)

Two-pass map: filesystem first (authoritative), longest-prefix as fallback for anything the filesystem pass missed.

```bash
# Discover products: all Umbraco.Automate* folders at repo root.
products=$(find . -maxdepth 1 -type d -name 'Umbraco.Automate*' | sed 's|^\./||')

# Inter-product packages: names from Directory.Packages.props
# (any PackageVersion whose name starts with Umbraco.Automate).
interProductPkgs=$(grep -Po 'PackageVersion Include="\K(Umbraco\.Automate[^"]*)' Directory.Packages.props)
```

**Pass 1 — filesystem ground truth** (each product folder owns the csprojs inside it):

```pseudo
packageToProduct = {}
for product in products:
    for csproj in glob(f"{product}/src/**/*.csproj"):
        pkgName = csproj.<PackageId> if set else basename(csproj).removesuffix(".csproj")
        packageToProduct[pkgName] = product
```

**Pass 2 — longest-prefix fallback** for anything in the inter-product list that pass 1 didn't see:

```pseudo
products_sorted = sorted(products, key=len, reverse=True)

mapByPrefix(pkg):
    for p in products_sorted:                   # longest first wins
        if pkg == p or pkg.startswith(p + "."):
            return p
    return None

for pkg in interProductPkgs:
    if pkg not in packageToProduct:
        owner = mapByPrefix(pkg)
        if owner is None:
            warn(f"Cannot resolve {pkg} to a product")
        else:
            packageToProduct[pkg] = owner
```

**Drift sanity check** — log where pass 1 and pass 2 disagree (pass 1 wins, but the divergence is informative):

```pseudo
for pkg, product in packageToProduct.items():
    prefixGuess = mapByPrefix(pkg)
    if prefixGuess and prefixGuess != product:
        info(f"{pkg} owned by {product} (filesystem); prefix would say {prefixGuess}")
```

#### Step 2 — Build the dependency graph

```pseudo
deps = {}                          # product -> set(products it depends on)
for product in products:
    deps[product] = set()
    for csproj in glob(f"{product}/src/**/*.csproj"):
        for ref in csproj.<PackageReference>:
            owner = packageToProduct.get(ref.Include)
            if owner and owner != product:
                deps[product].add(owner)

dependents = invert(deps)          # upstream -> set(downstream products)
```

#### Step 3 — Cascade forced patch bumps

```pseudo
TRIGGERS = {minor, major, downplayedBreaking}   # patch/none/fix do NOT cascade

queue = [p for p in bumpSet if bumpSet[p].bumpKind in TRIGGERS]

while queue not empty:
    upstream = queue.pop()
    for downstream in dependents.get(upstream, []):
        if downstream in bumpSet:
            continue          # already bumping; its own range update covers it
        bumpSet[downstream] = {
            currentVersion: read_version_json(downstream),
            newVersion:     patchBump(currentVersion),
            bumpKind:       "patch",
            forced:         True,
            forcedBy:       upstream,
        }
        # A forced patch does not change the downstream's API surface,
        # so it does not itself trigger further cascade.
```

`patchBump` mirrors `/post-release-cleanup` Phase 4 — reuse the same helper:
- `0.2.1` → `0.2.2`
- `1.0.0-beta1` → `1.0.0-beta2`
- `1.0.0-alpha` → `1.0.0-alpha.1`

#### Step 4 — Edge cases

1. **Forced product has no commits since its last tag.** Phase 9.5's "drop empty changelog" rule would otherwise remove it. Add an exception: forced bumps generate a stub entry like `### Internal\n- Bump to align with <upstream> <newVersion>` and stay in the release.

2. **User excludes a forced product from the manifest.** Phase 9.5 lets the user move products into `exclude`. Excluding a *forced* product breaks the cascade guarantee — old dependents on NuGet remain vulnerable to the stale-range trap. Warn loudly and require explicit confirmation.

3. **First-time products** (no prior release tag). Nothing on NuGet to be orphaned, so they can be skipped as upstreams in the cascade. As downstreams: standard handling.

#### Step 5 — Compute the manifest-affected set (separate from bumpSet)

CI's `detect-changes.ps1` flags any product whose csproj references a package whose range moved in `Directory.Packages.props` — including patch bumps. Cascade (Step 3) correctly does **not** add such products to `bumpSet` for patch upstreams (forward-compatible, no new release needed), but CI still requires those products to appear in either `include` or `exclude` of `release-manifest.json`. Without this acknowledgement, CI fails with `release-manifest.json is missing changed products: <Product>`.

Compute the broader "affected" set so Phase 8 can put it straight into `exclude`:

```pseudo
manifestAffected = set()                  # products that need acknowledgement only
for upstream in bumpSet:                  # every bump moves at least one range
    for downstream in dependents.get(upstream, []):
        if downstream not in bumpSet:
            manifestAffected.add(downstream)
```

This is the *broader* counterpart to Step 3's cascade. Step 3 produces `bumpSet ⊇ direct + forced` (everything that needs a new version). Step 5 produces `manifestAffected = (consumers of bumpSet via dep graph) − bumpSet` (everything that needs acknowledgement only).

Hand both sets to Phase 8.

### Phase 3: Version Confirmation

Present the **final** version table including any forced cascade bumps from Phase 2.5:

```
Final version set (including cascaded forced patches):

┌───────────────────────────────┬──────────┬──────────┬──────────────────────────────────────┐
│ Product                       │ Current  │ Proposed │ Reason                               │
├───────────────────────────────┼──────────┼──────────┼──────────────────────────────────────┤
│ Umbraco.Automate              │ 0.2.1    │ 0.3.0    │ 3 feat commits                       │
│ Umbraco.Automate.Slack        │ 0.2.1    │ 0.2.2    │ FORCED — depends on Umbraco.Automate │
│ Umbraco.Automate.OpenIddict   │ 0.1.0    │ 0.1.1    │ 1 fix commit                         │
└───────────────────────────────┴──────────┴──────────┴──────────────────────────────────────┘
```

Then use **AskUserQuestion** to confirm or adjust versions:

- **Default option**: "Use recommended versions (above)"
- **Alternative options**:
    - "Downplay breaking changes to minor" - Treat breaking changes as minor bumps (X.Y.0 → X.Y+1.0 instead of X+1.0.0). Cascade still applies (in fact, this option is only safe *because* cascade applies).
    - "Drop forced products" - Remove specific cascaded patches. Per Phase 2.5 Step 4, this requires explicit confirmation per product and breaks the cascade guarantee for those products.
    - "Adjust individual versions" - Manually specify version for each product
    - "Cancel release preparation"

**If user chooses "Downplay breaking changes to minor":**
- For all products with major bumps (X.Y.Z → X+1.0.0), change to minor bumps (X.Y.Z → X.Y+1.0)
- Keep all other bumps (minor, patch) as-is
- Show updated version table and confirm

**If user chooses "Adjust individual versions":**
- For each product, ask for custom version
- Validate version format (X.Y.Z)
- Warn if version doesn't follow semver conventions

### Phase 4: Update Inter-Product Dependency Ranges (.NET)

After confirming versions, update `Directory.Packages.props` so the dependency range for **every bumped product** reflects the new minimum (and, for major bumps, the new upper bound).

**Cascade prerequisite:** Phase 2.5 has already added forced patch bumps for every dependent of any minor/major/downplayed-breaking change. With cascade in place, every consumer that needs the new minimum is being released alongside, so raising the lower bound is safe to apply for *all* bumps — not just breaking ones. The earlier "only update for breaking changes" rule has been removed because it created a stale-range runtime trap whenever an upstream minor added an API the dependent then used.

**Rule (apply per bumped product P):**

| `bumpKind` | New range entry |
|---|---|
| `major` | `[X+1.0.0, X+1.999.999)` — both bounds change |
| `minor`, `downplayedBreaking`, `patch` (including forced) | `[newVersion, X.999.999)` — lower bound only |
| `none` | leave entry unchanged |

**Workflow:**

1. **Read current Directory.Packages.props** (look for inter-product `PackageVersion` entries — names starting with `Umbraco.Automate`).

   Note: until a product is published to NuGet, it may not have a `PackageVersion` entry yet (Slack currently consumes Core via `ProjectReference` for local builds). For products with no existing entry but which will be published in this release, add a new `PackageVersion` line as part of this phase.

2. **Compute proposed updates** by applying the rule above to every entry in `bumpSet`.

3. **Present proposed changes** to user:
   ```
   Directory.Packages.props updates:

   - Umbraco.Automate.Core:        [0.2.0, 0.999.999) → [0.3.0, 0.999.999)   (minor)
   - Umbraco.Automate.OpenIddict:  [0.1.0, 0.999.999) → [0.1.1, 0.999.999)   (patch)
   - Umbraco.Automate.Slack:       [0.2.1, 0.999.999) → [0.2.2, 0.999.999)   (forced patch — depends on Umbraco.Automate)
   ```

4. **Ask for approval** using AskUserQuestion:
   - **Default option**: "Apply these range updates (recommended)"
   - **Alternative options**:
     - "Skip range updates" — proceed without updating; warn that this will require manual fix-up before release
     - "Adjust manually later"

5. **If approved, update the file** with the Edit tool.

6. **Confirm updates**:
   ```
   ✓ Updated N inter-product dependency ranges in Directory.Packages.props
   ```

**Important Notes:**
- The rule deliberately does *not* exempt products without breaking changes. Phase 2.5's cascade ensures consumers are bumped alongside, so raising the lower bound is the safe default.
- Major-bump updates only affect the entries for products being released. Products excluded from the release manifest retain their old `.nuspec` ranges from prior releases — that is correct SemVer behavior (consumers can't pull an incompatible new major while still pinned to an old provider).
- Lower-bound bumps for `downplayedBreaking` are what make the downplay option safe: every dependent's new version requires the new upstream, so users upgrading through dependents always end up on a compatible pair.

### Phase 5: Create Release Branch

**IMPORTANT:** Create the release branch BEFORE making any file changes.

**Branch Naming Convention:**

Per CONTRIBUTING.md, every branch is version-prefixed and release branches are calendar-based:
- `vN/release/YYYY.MM.N` - Version line, year, month, and incrementing release number
- Example: `v18/release/2026.02.1` for the first February 2026 release on the v18 line
- Example: `v18/release/2026.02.2` for the second February 2026 release on the v18 line

This is independent from product version numbers (which follow semantic versioning). A single release branch like `v18/release/2026.02.1` can contain multiple products at different versions (e.g., Core@0.3.0, OpenIddict@0.1.1, Slack@0.2.2).

**Workflow:**

0. **Determine the version-line prefix** from the current branch — the release branch must stay on the same line:
   ```bash
   # e.g. on v18/dev → prefix=v18 ; on v17/dev → prefix=v17
   prefix=$(git branch --show-current | grep -oE '^v[0-9]+')
   ```
   If the current branch is not version-prefixed, ask the user which line to release.

1. **Determine current date** - Get current year and month for default branch name:
   ```bash
   date +"%Y.%m"
   ```

2. **Find next release number** - Check existing date-based release tags:
   ```bash
   git fetch --tags

   # IMPORTANT: This pattern only matches date-based release tags (YYYY.MM.COUNT),
   # not product version tags (Product@VERSION)
   git tag --list "2026.02.*" --sort=-version:refname
   ```

   **Tag Parsing Logic:**

   The repository uses **two types of tags**:
   - **Date-based release tags**: `YYYY.MM.N` (e.g., `2026.02.1`) - Represent complete release events
   - **Product version tags**: `Product@Version` (e.g., `Umbraco.Automate@0.2.0`) - Track individual product versions

   For release branch naming, we only care about date-based tags. The pattern `git tag --list "YYYY.MM.*"` only matches date-based tags, not product tags. Find the latest date tag for the current month and increment N. If no date tags exist for current month, N starts at 1.

3. **Ask user for branch name**:
   ```
   Create release branch using recommended calendar naming?

   Latest release tag for February 2026: 2026.02.2
   Suggested branch name: v18/release/2026.02.3 (next release in February 2026)

   Options:
   - Use suggested name (v18/release/2026.02.3)
   - Enter custom name (e.g., v18/release/v0.3.0 for version-based)
   - Cancel
   ```

4. **Create and checkout branch** (on the current line's prefix):
    ```bash
    git checkout -b $prefix/release/<name>
    ```

5. **Confirm branch creation**:
    ```
    ✓ Created and switched to branch: v18/release/2026.02.1

    All subsequent changes will be made on this branch.

    Note: This release will contain:
    - Umbraco.Automate 0.3.0
    - Umbraco.Automate.OpenIddict 0.1.1
    - Umbraco.Automate.Slack 0.2.2
    ```

### Phase 6: Dependency Validation

Reuse the dependency graph (`deps`, `dependents`, `packageToProduct`) built in Phase 2.5 to validate that the post-Phase-4 state is internally consistent. The validation covers **all** inter-product packages.

```pseudo
# 1. Each bumped product's newVersion satisfies its own (just-updated) range entry.
for product in bumpSet:
    range = parsed_range(Directory.Packages.props[product])
    if not range.contains(bumpSet[product].newVersion):
        error(f"{product} new version {bumpSet[product].newVersion} "
              f"is outside its declared range {range}")

# 2. For every minor/major/downplayed-breaking upstream, every consumer must be in
#    bumpSet (cascade should have ensured this — re-verify before committing).
for upstream in bumpSet:
    if bumpSet[upstream].bumpKind not in {major, minor, downplayedBreaking}:
        continue
    for downstream in dependents.get(upstream, []):
        if downstream not in bumpSet:
            error(f"Cascade gap: {downstream} depends on {upstream} "
                  f"({bumpSet[upstream].bumpKind} bump) but is not in bumpSet. "
                  f"Old {downstream} on NuGet may resolve to an incompatible "
                  f"{upstream}.")

# 3. Major-bump conflicts: any out-of-release product whose csproj references an
#    upstream undergoing a new major must be addressed explicitly.
for upstream in bumpSet:
    if bumpSet[upstream].bumpKind != major:
        continue
    for downstream in all_products:
        if downstream in bumpSet:
            continue
        for ref in csproj_refs(downstream):
            if packageToProduct.get(ref) == upstream:
                error(f"{downstream} (not in release) depends on {upstream}, "
                      f"which is going to a new major. Either include "
                      f"{downstream} with a compatible bump or hold {upstream} "
                      f"at the current major for this release.")
```

Present results:

```
✅ All bumped products satisfy their declared ranges
✅ Cascade closed: every consumer of a minor/major/downplayed upstream is in the release
🔴 Umbraco.Automate.Slack (not in release) depends on Umbraco.Automate which is going to 1.0.0
   → must be addressed before continuing
```

If hard errors exist, ask the user how to proceed — common resolutions:

- Include the missing product (re-run Phase 2.5 cascade with it added)
- Hold the upstream at its current major (revert that bump in Phase 3)
- Abort and fix manually

### Phase 7: Update version.json Files

For each product with confirmed version:

1. **Read current version.json**:
    ```json
    {
        "version": "0.2.0",
        "suffixes": ["-beta", ""]
    }
    ```

2. **Update version field** using Edit tool

3. **Verify update** by reading the file back

### Phase 8: Generate Release Manifest

Build the manifest from three sources so CI's `detect-changes.ps1` accepts it:

```pseudo
include = sorted(bumpSet keys)

exclude = sorted(union of:
    - Phase 1 changed products you chose NOT to release  (file changes, dropped)
    - Phase 2.5 Step 5 manifestAffected                  (range-delta consumers)
    - First-time products with commits since main not in include
)
```

Every product CI considers "changed" by either mechanism (file diff or `Directory.Packages.props` range delta) **must** appear in one of the two lists.

If `/release-manifest-management` is invoked without an explicit `--exclude=`, write `release-manifest.json` directly using the Write tool with the object format:

```json
{
    "include": [...],
    "exclude": [...]
}
```

Or invoke the skill with the include list and patch the exclude list yourself afterwards.

**Sanity check before moving on:**

```pseudo
for product in all_products:
    isChanged = (product in Phase1Changed) or (product in manifestAffected) or (product in bumpSet)
    if isChanged and product not in include and product not in exclude:
        error(f"{product} will fail CI validation — add to include or exclude")
```

Run this check before Phase 11 commits. CI's failure message looks like:
> `release-manifest.json is missing changed products (add to 'include' or 'exclude'): <Product>`

### Phase 9: Generate Changelogs

For each product in the manifest:

1. **Invoke `/changelog-management`** skill:
    ```bash
    /changelog-management --product=<ProductName> --version=<Version>
    ```

2. **Verify changelog** was generated correctly

### Phase 9.5: Changelog Review

After generating changelogs, review each product's new version entry for quality and completeness.

#### Step 1: Noise Detection

For each product's changelog entry, check for and flag:

1. **Internal-only entries that shouldn't be public:**
   - `refactor` sections — these are hidden from changelogs per convention (types hidden: refactor, chore, docs, test, ci, build). If the generator included them, they should be removed
   - Test-only fixes (e.g., "Fix failing tests") — not user-facing
   - Build/CI changes (e.g., "Exclude dev files from NuGet package")
   - Generated code changes (e.g., "Fix casing in generated OpenAPI client")

2. **Leaked metadata:**
   - `Co-Authored-By` lines in commit bodies that leaked into changelog entries
   - PR merge commit noise
   - Internal tool references (e.g., "after /simplify review")

3. **Duplicate cross-product entries:**
   - Same commit appearing in multiple product changelogs — verify it's actually relevant to each product

#### Step 2: Completeness Check

For each product being released, verify the changelog captured all meaningful changes:

1. **Compare changelog entries against actual file changes:**
   ```bash
   git diff <tag>..HEAD --name-only -- <ProductFolder>/ | grep -v 'CHANGELOG.md\|version.json'
   git log <tag>..HEAD --pretty=format:"%h %s" -- <ProductFolder>/
   ```

2. **Look for missing entries** — commits scoped to other products (e.g., `feat(slack)`) that touched this product's files won't be captured by the changelog generator since it filters by scope. Common cases:
   - Cross-product features where the commit scope doesn't match all affected products
   - Batch commits that span multiple product directories

3. **For each missing change, determine if it's changelog-worthy:**
   - Does it change user-facing behavior? → Add a manually written entry
   - Is it only adapting to a dependency change? → Skip (internal plumbing)
   - Is it a meaningful new feature/fix that users should know about? → Add entry

#### Step 3: Empty Changelog Assessment

For products with **empty changelog entries** (version header but no content):

1. **Review what actually changed** in the product directory:
   ```bash
   git diff <tag>..HEAD --stat -- <ProductFolder>/ | grep -v 'CHANGELOG.md\|version.json'
   ```

2. **Categorize the changes:**
   - **Build/tooling only** (lock files, csproj conditions, slnx migrations, CLAUDE.md) → Recommend dropping from release
   - **Dependency adaptation** (adapting to API changes in a dependency, no new features) → Recommend dropping OR add a brief entry
   - **Real features/fixes** that were missed by the generator → Add entries manually

3. **Forced cascade products are an exception** (per Phase 2.5 Step 4): they may legitimately have no commits since their last tag. For these, add a stub entry like `### Internal\n- Bump to align with <upstream> <newVersion>` and keep them in the release.

4. **If recommending to drop a non-forced product**, update the release manifest and revert files:
   - Move it from `include` to `exclude` in `release-manifest.json`
   - **IMPORTANT:** Every changed product that is NOT being released MUST appear in the `exclude` list. CI's change detection script checks all products with changes since their last tag — any changed product missing from both `include` and `exclude` will fail the build.
   - Revert its `version.json` to the pre-release value
   - Revert its `CHANGELOG.md` changes

#### Step 4: Present Review

Present findings to the user organized by severity:

```
Changelog Review Results:

🔴 Issues requiring attention:
- [Product]: Empty changelog — only build changes detected, recommend dropping
- [Product]: Missing entry for [feature] — cross-product commit scoped elsewhere

🟡 Noise to clean up:
- [Product]: Refactor section should be removed (14 entries)
- [Product]: "Fix failing tests" entry is internal-only
- [Product]: Co-Authored-By leaked into breaking change body

✅ Clean:
- [Product]: Changelog looks good
- [Product]: Changelog looks good
```

#### Step 5: Apply Fixes

After user confirms:

1. **Remove noise entries** — Edit changelogs to remove flagged items
2. **Add missing entries** — Write manually crafted entries for missed changes
3. **Drop products** — Update release manifest, revert versions
4. **Re-validate** — Ensure all changelogs still have content after cleanup

#### Important Notes

- This phase is about **editorial quality**, not format validation (that's Phase 10)
- The changelog generator works by matching commit scopes to products. Cross-product commits with a single scope will only appear in that scope's changelog. This is a known limitation — manual additions are expected.
- When adding manual entries, follow the same format as generated entries but use a scope matching the product being edited
- Refactor/chore/docs types should NEVER appear in public changelogs per project convention

### Phase 10: Validation

Verify all files are consistent:

1. **Check version.json** matches intended versions
2. **Check CHANGELOG.md** exists and has correct version header
3. **Check release-manifest.json** includes all intended products
4. **Run Phase 8 sanity check** — every "changed" product appears in include or exclude
5. **Report any issues** to user

### Phase 11: Commit Changes

**All work has been done on the release branch.** Now commit everything:

1. **Stage all changes**:
    ```bash
    git add release-manifest.json
    git add Directory.Packages.props
    git add */version.json
    git add */CHANGELOG.md
    ```

2. **Create commit**:
    ```bash
    git commit -m "chore(release): Prepare release 2026.02.1

    Updated products:
    - Umbraco.Automate: 0.2.1 → 0.3.0
    - Umbraco.Automate.OpenIddict: 0.1.0 → 0.1.1
    - Umbraco.Automate.Slack: 0.2.1 → 0.2.2 (forced — depends on Umbraco.Automate)

    Co-Authored-By: Claude <noreply@anthropic.com>"
    ```

    **Note:** Use the release branch name (e.g., 2026.02.1) in the commit message, not product versions.

3. **Show summary**:
    ```
    ✓ Release branch created: v18/release/2026.02.1
    ✓ Updated 3 products:
      - Umbraco.Automate: 0.2.1 → 0.3.0
      - Umbraco.Automate.OpenIddict: 0.1.0 → 0.1.1
      - Umbraco.Automate.Slack: 0.2.1 → 0.2.2 (forced)
    ✓ Generated changelogs
    ✓ All changes committed

    Next steps:
    - Review the changes: git show HEAD
    - Push to remote: git push -u origin $prefix/release/2026.02.1
    - Create PR to merge into main
    - CI will validate and build packages
    ```

## Important Notes

- Always run from repository root
- **Branch naming**: Use version-prefixed calendar naming `vN/release/YYYY.MM.N` (per CONTRIBUTING.md)
  - The `vN` prefix matches the line you are releasing from (e.g. `v18/dev` → `v18/release/...`)
  - Independent from product versions (multiple products = different versions in one release)
  - N is an incrementing number for each release in that month (1, 2, 3, etc.)
  - Example: `v18/release/2026.02.1` can contain Core@0.3.0, OpenIddict@0.1.1, Slack@0.2.2
  - Version-based naming like `v18/release/v0.3.0` is valid but not recommended
- Use conventional commit analysis for version recommendations
- Validate cross-product dependencies
- This skill orchestrates `/release-manifest-management` and `/changelog-management`
- Creates commits following conventional commit format
- Release branches trigger CI validation and packaging

## Version Bump Decision Logic

```
Priority (highest first):
1. BREAKING CHANGE in commit body → Major
2. ! after scope (e.g., feat!:) → Major
3. feat: or feat(<scope>): → Minor
4. fix: or perf: → Patch
5. Only docs/chore/refactor → Ask user (default: patch)
```

## Cross-Product Dependency Check

The dependency graph used for cascade (Phase 2.5) and validation (Phase 6) is built dynamically from:

1. Inter-product `PackageVersion` entries in root `Directory.Packages.props` (names starting with `Umbraco.Automate`)
2. Each `<Product>/src/**/*.csproj` file's `<PackageReference>` entries (also including those resolved via `<ProjectReference>` for local builds — once a product publishes to NuGet, the dep becomes real)

The `packageToProduct` map is built filesystem-first (each product folder owns the csprojs inside it) with a longest-prefix fallback for any inter-product package not represented by a csproj in the current checkout.

Range entries follow the form:

```xml
<PackageVersion Include="Umbraco.Automate.Core" Version="[0.2.0, 0.999.999)" />
```

Range updates follow the simplified Phase 4 rule (lower bound on every bump; both bounds on majors), with cascade ensuring no consumer is left behind.

## Example Flow

```
User invokes: /release-management

Phase 1: Detect changes
You scan git history and show:
- Umbraco.Automate: 12 commits since 0.2.1
- Umbraco.Automate.OpenIddict: 3 commits since 0.1.0
- Umbraco.Automate.Slack: 0 commits since 0.2.1

Phase 2: Analyze versions
You show recommendations:
- Umbraco.Automate: 0.2.1 → 0.3.0 (minor - 3 feat commits)
- Umbraco.Automate.OpenIddict: 0.1.0 → 0.1.1 (patch - 1 fix)

Phase 2.5: Cascade analysis
You build the package→product map (filesystem + longest-prefix fallback)
You build the dep graph from each csproj's PackageReferences
TRIGGERS = {minor, major, downplayedBreaking}
- Umbraco.Automate is minor → cascade to its dependents
- Umbraco.Automate.OpenIddict is patch → no cascade
Forced patches added (Step 3):
- Umbraco.Automate.Slack: 0.2.1 → 0.2.2 (depends on Umbraco.Automate minor)
manifestAffected computed (Step 5):
- (in this scenario, the only consumer is already in bumpSet → empty)

Phase 3: Confirm versions
You show the FINAL set including forced cascade entries.
Options:
- Use recommended versions (above)
- Downplay breaking changes to minor (cascade still applies)
- Drop forced products (per-product, with warning)
- Adjust individual versions
- Cancel
User confirms

Phase 4: Update Directory.Packages.props
You apply the simplified rule per bumped product:
- Major: both bounds change
- Minor / downplayedBreaking / patch (incl. forced): lower bound only
- none: leave alone
User approves updates.

Phase 5: Create release branch
You fetch tags, check YYYY.MM.* tags
You suggest: release/2026.05.1
You create the branch and switch to it

Phase 6: Validate dependencies
You re-use the dep graph from Phase 2.5 to validate the post-Phase-4 state.
You verify: every consumer of a minor/major/downplayed upstream is in bumpSet.
You verify: every bumped newVersion satisfies its just-updated range entry.
No errors found.

Phase 7: Update version.json
You edit all three version.json files on the release branch

Phase 8: Generate manifest
You build include from bumpSet and exclude from
(Phase 1 dropped) ∪ manifestAffected ∪ (first-time non-bumped)
You sanity-check: every "changed" product is in one list or the other
Manifest written using the object format

Phase 9: Generate changelogs
You invoke /changelog-management for each product
For Slack (forced, no commits): stub entry "Bump to align with Umbraco.Automate 0.3.0"

Phase 9.5: Changelog review
You review each product's changelog for noise and completeness
You flag any issues, present review results to user
User confirms fixes
You apply any needed cleanup

Phase 10: Validate
You verify all files are correct

Phase 11: Commit changes
You commit all changes to the release branch
You show summary and next steps
```

## Error Handling

- **No changes detected**: Ask user if they want to proceed anyway (manual version bump)
- **Git tag not found** (`git describe` exits non-zero — no reachable product tag on this line): Fall back to `git merge-base origin/$prefix/main HEAD` (the current line's main, e.g. `origin/v18/main`) and compare against that, mirroring CI's detect-changes.ps1 fallback. Do NOT fall back to a version-sorted `git tag --list` lookup, and do NOT use bare `origin/main` — both reintroduce the parallel-line bug described in Phase 1.
- **Invalid version.json**: Report error and ask user to fix manually
- **Changelog generation fails**: Report error but continue with other products
- **Dependency conflict**: Warn user but allow them to proceed
- **Cascade gap detected in Phase 6**: Hard error — include the missing dependent, hold the upstream, or abort

