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.
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.
You are the orchestrator for preparing releases in the Umbraco.Automate repository.
Task
Guide users through the complete release preparation process:
Detect changed products since their last release tags
Analyze commits to recommend version bumps (major/minor/patch)
Cascade forced bumps to every dependent of a product undergoing minor/major/downplayed-breaking changes
Confirm versions with the user (including cascaded products)
Update Directory.Packages.props inter-product dependency ranges (with user approval)
Create release branch (e.g., v18/release/2026.02.1) and switch to it
Dependency validation - Check for cross-product conflicts using the graph built in cascade analysis
Update version.json files for each product
Generate release-manifest.json via /release-manifest-management
Generate CHANGELOG.md files via /changelog-management
Changelog review - Review generated changelogs for quality and completeness
Validate all files are consistent
Commit all changes to the release branch
Workflow
Phase 1: Change Detection
Find all products - Discover Umbraco.Automate* directories at repo root
For each product, detect changes since last release:
# 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'
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.
# 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):
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:
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):
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
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
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
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.
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.
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:
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:
"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)
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.
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.
Compute proposed updates by applying the rule above to every entry in bumpSet.
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"
If approved, update the file with the Edit tool.
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:
Determine the version-line prefix from the current branch — the release branch must stay on the same line:
# 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.
Determine current date - Get current year and month for default branch name:
date +"%Y.%m"
Find next release number - Check existing date-based release tags:
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
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.
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
Create and checkout branch (on the current line's prefix):
git checkout -b $prefix/release/<name>
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.
# 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:
Read current version.json:
{"version":"0.2.0","suffixes":["-beta",""]}
Update version field using Edit tool
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:
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:
{"include":[...],"exclude":[...]}
Or invoke the skill with the include list and patch the exclude list yourself afterwards.
Sanity check before moving on:
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>
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:
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")
Leaked metadata:
Co-Authored-By lines in commit bodies that leaked into changelog entries
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
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):
Review what actually changed in the product directory:
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
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.
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:
Remove noise entries — Edit changelogs to remove flagged items
Drop products — Update release manifest, revert versions
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:
Check version.json matches intended versions
Check CHANGELOG.md exists and has correct version header
Check release-manifest.json includes all intended products
Run Phase 8 sanity check — every "changed" product appears in include or exclude
Report any issues to user
Phase 11: Commit Changes
All work has been done on the release branch. Now commit everything:
Note: Use the release branch name (e.g., 2026.02.1) in the commit message, not product versions.
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:
Inter-product PackageVersion entries in root Directory.Packages.props (names starting with Umbraco.Automate)
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 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