diff --git a/automation/utils/bin/rui-release-info.ts b/automation/utils/bin/rui-release-info.ts new file mode 100755 index 0000000000..e9f604be94 --- /dev/null +++ b/automation/utils/bin/rui-release-info.ts @@ -0,0 +1,67 @@ +#!/usr/bin/env ts-node +import { loadReleaseCandidates, loadAllPackages } from "../src/release-candidates"; + +async function main(): Promise { + const args = process.argv.slice(2); + const command = args[0]; + + try { + if (command === "--candidates" || command === "-c") { + // List only packages with unreleased changes + const candidates = await loadReleaseCandidates(); + console.log(JSON.stringify(candidates, null, 2)); + } else if (command === "--all" || command === "-a") { + // List all packages (including those without changes) + const allPackages = await loadAllPackages(); + console.log(JSON.stringify(allPackages, null, 2)); + } else if (command === "--summary" || command === "-s") { + // Summary view + const candidates = await loadReleaseCandidates(); + const summary = { + totalCandidates: candidates.length, + widgets: candidates.filter(c => c.packageType === "widget").length, + modules: candidates.filter(c => c.packageType === "module").length, + packages: candidates.map(c => ({ + name: c.name, + packageType: c.packageType, + hasDependencies: c.hasDependencies, + version: c.currentVersion, + hasChanges: c.hasUnreleasedChanges, + dependentWidgetsWithChanges: c.hasDependencies + ? c.dependentWidgets!.filter(w => w.hasUnreleasedChanges).length + : undefined + })) + }; + console.log(JSON.stringify(summary, null, 2)); + } else if (command === "--help" || command === "-h" || !command) { + printHelp(); + } else { + console.error(`Unknown command: ${command}`); + console.error("Use --help for usage information"); + process.exit(1); + } + } catch (error) { + console.error("Error:", error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +function printHelp(): void { + console.log(` +rui-release-info - Query release candidates in the monorepo + +Commands: + -c, --candidates List packages with unreleased changes (release candidates) + Returns detailed JSON with changelog entries + + -a, --all List all packages (including those without changes) + Useful for seeing complete package structure + + -s, --summary Show summary statistics and package list + Concise view with counts and basic info + + -h, --help Show this help message +`); +} + +main(); diff --git a/automation/utils/package.json b/automation/utils/package.json index 6754b37c84..5a6f47ee55 100644 --- a/automation/utils/package.json +++ b/automation/utils/package.json @@ -11,6 +11,7 @@ "rui-include-oss-in-artifact": "bin/rui-include-oss-in-artifact.ts", "rui-prepare-release": "bin/rui-prepare-release.ts", "rui-publish-marketplace": "bin/rui-publish-marketplace.ts", + "rui-release-info": "bin/rui-release-info.ts", "rui-update-changelog-module": "bin/rui-update-changelog-module.ts", "rui-update-changelog-widget": "bin/rui-update-changelog-widget.ts", "rui-verify-package-format": "bin/rui-verify-package-format.ts" diff --git a/automation/utils/src/index.ts b/automation/utils/src/index.ts index 8e405bb4e8..a8cc059505 100644 --- a/automation/utils/src/index.ts +++ b/automation/utils/src/index.ts @@ -6,3 +6,5 @@ export * from "./mpk"; export * from "./changelog-parser"; export * from "./monorepo"; export * from "./build-config"; +export * from "./release-candidates"; +export * from "./io/filesystem"; diff --git a/automation/utils/src/io/filesystem.ts b/automation/utils/src/io/filesystem.ts new file mode 100644 index 0000000000..2e504e2929 --- /dev/null +++ b/automation/utils/src/io/filesystem.ts @@ -0,0 +1,43 @@ +import { access, readdir, readFile, writeFile } from "fs/promises"; + +/** + * Abstraction layer for filesystem operations + * Allows for testing and alternative implementations (e.g., MCP, in-memory) + */ +export interface FileSystem { + readFile(path: string): Promise; + writeFile(path: string, content: string): Promise; + exists(path: string): Promise; + readdir(path: string): Promise; +} + +/** + * Default Node.js filesystem implementation + */ +export class NodeFileSystem implements FileSystem { + async readFile(path: string): Promise { + return readFile(path, "utf-8"); + } + + async writeFile(path: string, content: string): Promise { + await writeFile(path, content, "utf-8"); + } + + async exists(path: string): Promise { + try { + await access(path); + return true; + } catch { + return false; + } + } + + async readdir(path: string): Promise { + return readdir(path); + } +} + +/** + * Default filesystem instance for convenience + */ +export const defaultFS = new NodeFileSystem(); diff --git a/automation/utils/src/release-candidates.ts b/automation/utils/src/release-candidates.ts new file mode 100644 index 0000000000..761a5a0a9b --- /dev/null +++ b/automation/utils/src/release-candidates.ts @@ -0,0 +1,296 @@ +import { join } from "path"; +import { + getWidgetChangelog, + getModuleChangelog, + WidgetChangelogFileWrapper, + ModuleChangelogFileWrapper +} from "./changelog-parser"; +import { FileSystem, defaultFS } from "./io/filesystem"; +import { getPackageInfo, getWidgetInfo, getModuleInfo, PackageInfo } from "./package-info"; + +/** + * Represents a single changelog entry + */ +export interface ChangelogEntry { + type: "Fixed" | "Added" | "Changed" | "Removed"; + description: string; +} + +/** + * Release information for a dependent widget + */ +export interface DependentWidgetInfo { + name: string; + path: string; + currentVersion: string; + appName: string; + hasUnreleasedChanges: boolean; + unreleasedEntries: ChangelogEntry[]; +} + +/** + * Release candidate - represents any package that can be released + */ +export interface ReleaseCandidate { + packageType: "widget" | "module"; + hasDependencies: boolean; + name: string; + path: string; + currentVersion: string; + appNumber: number; + appName: string; + hasUnreleasedChanges: boolean; + unreleasedEntries: ChangelogEntry[]; + dependentWidgets?: DependentWidgetInfo[]; // Only present if hasDependencies is true +} + +/** + * Check if a package is an aggregator (has dependencies and appNumber) + */ +function isAggregator(info: PackageInfo): boolean { + return !!info.marketplace.appNumber && info.mxpackage.dependencies && info.mxpackage.dependencies.length > 0; +} + +/** + * Check if a package is independent widget (has appNumber, no dependencies, is a widget) + */ +function isIndependentWidget(info: PackageInfo): boolean { + return ( + !!info.marketplace.appNumber && + (!info.mxpackage.dependencies || info.mxpackage.dependencies.length === 0) && + info.mxpackage.type === "widget" + ); +} + +/** + * Check if a package is a standalone module (has appNumber, no dependencies, is a module) + * Example: web-actions (just JavaScript actions, no widgets) + */ +function isStandaloneModule(info: PackageInfo): boolean { + return ( + !!info.marketplace.appNumber && + (!info.mxpackage.dependencies || info.mxpackage.dependencies.length === 0) && + info.mxpackage.type === "module" + ); +} + +/** + * Extract simple changelog entries from changelog wrapper + */ +export function extractChangelogEntries( + changelog: WidgetChangelogFileWrapper | ModuleChangelogFileWrapper +): ChangelogEntry[] { + const unreleased = changelog.changelog.content[0]; + return unreleased.sections.flatMap(section => + section.logs.map(log => ({ + type: section.type, + description: log + })) + ); +} + +/** + * Load dependent widget info + * Note: Some "widgets" might actually be other types (e.g., jsactions) + * We need to check the package type first + */ +async function loadDependentWidgetInfo(path: string): Promise { + const basicInfo = await getPackageInfo(path); + + // Only process actual widgets + if (basicInfo.mxpackage.type !== "widget") { + throw new Error(`Package ${basicInfo.name} is not a widget (type: ${basicInfo.mxpackage.type})`); + } + + const info = await getWidgetInfo(path); + const changelog = await getWidgetChangelog(path); + const hasUnreleasedChanges = changelog.hasUnreleasedLogs(); + const unreleasedEntries = hasUnreleasedChanges ? extractChangelogEntries(changelog) : []; + + return { + name: info.name, + path, + currentVersion: info.version.format(), + appName: info.marketplace.appName ?? info.mxpackage.name, + hasUnreleasedChanges, + unreleasedEntries + }; +} + +/** + * Scan a directory for packages + */ +async function scanPackagesDirectory(dirPath: string, fs: FileSystem = defaultFS): Promise { + try { + const exists = await fs.exists(dirPath); + if (!exists) return []; + + const entries = await fs.readdir(dirPath); + const packagePaths: string[] = []; + + for (const entry of entries) { + const fullPath = join(dirPath, entry); + const packageJsonPath = join(fullPath, "package.json"); + const hasPackageJson = await fs.exists(packageJsonPath); + + if (hasPackageJson) { + packagePaths.push(fullPath); + } + } + + return packagePaths; + } catch (error) { + console.warn(`Warning: Could not scan directory ${dirPath}:`, error); + return []; + } +} + +/** + * Load release candidates (packages with unreleased changes) + * @param rootPath - Root path of the monorepo (defaults to cwd) + * @param fs - Filesystem implementation (defaults to Node.js fs) + * @returns Array of release candidates that have unreleased changes + */ +export async function loadReleaseCandidates( + rootPath: string = process.cwd(), + fs: FileSystem = defaultFS +): Promise { + // Load all packages first + const allPackages = await loadAllPackages(rootPath, fs); + + // Filter to only those with unreleased changes + return allPackages.filter(pkg => { + if (pkg.hasDependencies) { + // For packages with dependencies, check if package itself OR any dependent has changes + return pkg.hasUnreleasedChanges || pkg.dependentWidgets!.some(w => w.hasUnreleasedChanges); + } else { + // For independent packages, just check the package itself + return pkg.hasUnreleasedChanges; + } + }); +} + +/** + * Load all packages (including those without unreleased changes) + * Loads all releasable packages from the monorepo + * @param rootPath - Root path of the monorepo (defaults to cwd) + * @param fs - Filesystem implementation (defaults to Node.js fs) + * @returns Array of all release candidates + */ +export async function loadAllPackages( + rootPath: string = process.cwd(), + fs: FileSystem = defaultFS +): Promise { + const candidates: ReleaseCandidate[] = []; + + // Scan pluggableWidgets directory + const widgetsDir = join(rootPath, "packages", "pluggableWidgets"); + const widgetPaths = await scanPackagesDirectory(widgetsDir, fs); + + // Scan modules directory + const modulesDir = join(rootPath, "packages", "modules"); + const modulePaths = await scanPackagesDirectory(modulesDir, fs); + + // Track dependent widgets to skip them in the first pass + const dependentWidgets = new Set(); + + // First pass: identify all packages and their relationships + const allPackages = new Map(); + + for (const path of [...widgetPaths, ...modulePaths]) { + try { + const info = await getPackageInfo(path); + allPackages.set(info.name, { path, info }); + + // If this is an aggregator, mark its dependencies + if (isAggregator(info)) { + info.mxpackage.dependencies.forEach(dep => dependentWidgets.add(dep)); + } + } catch (error) { + console.warn(`Warning: Could not load package info for ${path}:`, error); + } + } + + // Second pass: process all packages (no filtering here) + for (const [name, { path, info }] of allPackages.entries()) { + try { + if (isAggregator(info)) { + // Package with dependencies (module or widget aggregator) + const packageType = info.mxpackage.type as "widget" | "module"; + + // Load changelog (both use module format) + const changelog = await getModuleChangelog(path, info.mxpackage.name); + const hasOwnChanges = changelog.hasUnreleasedLogs(); + const ownEntries = hasOwnChanges ? extractChangelogEntries(changelog) : []; + + // Load dependent widgets + const dependentWidgetInfos: DependentWidgetInfo[] = []; + for (const depName of info.mxpackage.dependencies) { + const depPackage = allPackages.get(depName); + if (depPackage) { + try { + const depInfo = await loadDependentWidgetInfo(depPackage.path); + dependentWidgetInfos.push(depInfo); + } catch (error) { + console.warn(`Warning: Could not load dependent widget ${depName}:`, error); + } + } + } + + candidates.push({ + packageType, + hasDependencies: true, + name: info.name, + path, + currentVersion: info.version.format(), + appNumber: info.marketplace.appNumber!, + appName: info.marketplace.appName ?? info.mxpackage.name, + hasUnreleasedChanges: hasOwnChanges, + unreleasedEntries: ownEntries, + dependentWidgets: dependentWidgetInfos + }); + } else if (isIndependentWidget(info) && !dependentWidgets.has(name)) { + // Independent widget (no dependencies) + const widgetInfo = await getWidgetInfo(path); + const changelog = await getWidgetChangelog(path); + const hasUnreleasedChanges = changelog.hasUnreleasedLogs(); + const unreleasedEntries = hasUnreleasedChanges ? extractChangelogEntries(changelog) : []; + + candidates.push({ + packageType: "widget", + hasDependencies: false, + name: info.name, + path, + currentVersion: widgetInfo.version.format(), + appNumber: info.marketplace.appNumber!, + appName: info.marketplace.appName ?? info.mxpackage.name, + hasUnreleasedChanges, + unreleasedEntries + }); + } else if (isStandaloneModule(info)) { + // Standalone module (no dependencies) + const moduleInfo = await getModuleInfo(path); + const changelog = await getModuleChangelog(path, info.mxpackage.name); + const hasUnreleasedChanges = changelog.hasUnreleasedLogs(); + const unreleasedEntries = hasUnreleasedChanges ? extractChangelogEntries(changelog) : []; + + candidates.push({ + packageType: "module", + hasDependencies: false, + name: info.name, + path, + currentVersion: moduleInfo.version.format(), + appNumber: info.marketplace.appNumber!, + appName: info.marketplace.appName ?? info.mxpackage.name, + hasUnreleasedChanges, + unreleasedEntries + }); + } + // Skip dependent widgets - they're handled as part of aggregators + } catch (error) { + console.warn(`Warning: Could not process package ${name}:`, error); + } + } + + return candidates; +} diff --git a/docs/release-process/release-info-tool-usage.md b/docs/release-process/release-info-tool-usage.md new file mode 100644 index 0000000000..d93f4f6029 --- /dev/null +++ b/docs/release-process/release-info-tool-usage.md @@ -0,0 +1,306 @@ +## Determining Releasability + +### Data Structure for Release Candidates + +See `docs/release-process/release-info-tool.md` for the complete reference. + +```typescript +interface ReleaseCandidate { + packageType: "widget" | "module"; // From package.json mxpackage.type + hasDependencies: boolean; // Does it bundle other widgets? + name: string; // NPM package name + path: string; // Filesystem path + currentVersion: string; // Current version (e.g., "3.9.0") + appNumber: number; // Mendix Marketplace app number + appName: string; // Display name in Marketplace + hasUnreleasedChanges: boolean; // Does the package itself have changes? + unreleasedEntries: ChangelogEntry[]; // Changelog entries for this package + dependentWidgets?: DependentWidgetInfo[]; // Only present if hasDependencies is true +} + +interface ChangelogEntry { + type: "Fixed" | "Added" | "Changed" | "Removed" | "Breaking changes"; + description: string; +} + +interface DependentWidgetInfo { + name: string; + path: string; + currentVersion: string; + appName: string; + hasUnreleasedChanges: boolean; + unreleasedEntries: ChangelogEntry[]; +} +``` + +### Algorithm for determining release candidates + +Use the `rui-release-info` tool: + +```bash +# Get packages with unreleased changes +pnpm exec ts-node automation/utils/bin/rui-release-info.ts --candidates +``` + +**Logic**: + +``` +FOR EACH package in [packages/pluggableWidgets/*, packages/modules/*]: + pkg = read package.json + + IF pkg.marketplace.appNumber does NOT exist: + → Skip (dependent widget, handled by its aggregator) + + IF pkg.mxpackage.dependencies exists AND length > 0: + # Aggregator (packageType: "widget" or "module", hasDependencies: true) + hasChanges = (package changelog has unreleased entries) OR + (any dependent widget has unreleased entries) + + IF hasChanges: + → Add to release candidates with dependentWidgets array + ELSE: + # Standalone (packageType: "widget" or "module", hasDependencies: false) + IF changelog has unreleased entries: + → Add to release candidates +``` + +See `automation/utils/src/release-candidates.ts` for the implementation. + +## Dependencies and Constraints + +### Dependency structure + +- **Aggregators (`hasDependencies: true`) → Widgets**: Both module and widget aggregators can bundle widgets + - Module aggregators (e.g., `data-widgets` → `datagrid-web`) + - Widget aggregators (e.g., `charts-web` → `line-chart-web`) - only one example exists +- **No deeper nesting**: Maximum 2 levels, no aggregator-to-aggregator dependencies +- **No circular dependencies**: A widget cannot be both standalone and dependent + +**Key identifier**: `hasDependencies: true` means package has `mxpackage.dependencies` array with items AND `marketplace.appNumber`. + +### Version synchronization + +- All widgets in an aggregator MUST have same version +- Version is determined by aggregator version +- Even unchanged widgets get version bumps + +### Package.json vs package.xml + +- `package.json`: Source of truth for version +- `src/package.xml`: Widget-specific, must stay in sync +- Modules don't have `package.xml` +- Scripts handle synchronization automatically + +## Existing Automation Tools + +Located in `automation/utils/`: + +### Package Information + +- `src/package-info.ts`: TypeScript types and parsers for package.json +- `src/monorepo.ts`: Utilities for working with pnpm workspace + +### Changelog Operations + +- `src/changelog-parser/`: Parser and writer for CHANGELOG.md files +- `src/changelog.ts`: High-level changelog operations +- `bin/rui-update-changelog-widget.ts`: Update widget changelog +- `bin/rui-update-changelog-module.ts`: Update module changelog + +### Version Bumping + +- `src/bump-version.ts`: Bump versions in package.json and package.xml +- `src/version.ts`: Version parsing and manipulation + +### Release Preparation + +- `bin/rui-prepare-release.ts`: Interactive wizard for release preparation +- `src/prepare-release-helpers.ts`: Helper functions for release prep + +### Release Information + +- `bin/rui-release-info.ts`: CLI tool to query release candidates (see `docs/requirements/release-info-tool-summary.md`) +- `src/release-candidates.ts`: Core logic for identifying releasable packages +- `src/io/filesystem.ts`: Filesystem abstraction for testing + +### Other Tools + +- `bin/rui-check-changelogs.ts`: Validate changelog format +- `bin/rui-oss-clearance.ts`: OSS clearance workflow +- `bin/rui-create-gh-release.ts`: Create GitHub releases +- `bin/rui-publish-marketplace.ts`: Publish to Mendix Marketplace + +## Future Automation Goals + +### Release Agent Capabilities + +1. ✅ **Discover releasable packages**: Use `rui-release-info --candidates` to scan monorepo and identify packages with unreleased changes +2. **Suggest version bumps**: Analyze changelog entries, suggest patch/minor/major based on entry types +3. **Check for related work**: Query Jira for related stories, suggest bundling multiple changes +4. **Validate release readiness**: Check CI status, changelog format, required files +5. **Execute release preparation**: Bump versions, create branch, trigger workflow (reuse existing `rui-prepare-release` logic) +6. **Monitor release status**: Track workflow progress, OSS clearance, publication + +### Integration Approaches + +**CLI Tool** (✅ implemented): + +- `rui-release-info` script outputs JSON +- Called via Bash tool in agent workflows +- Reusable functions in `@mendix/automation-utils` + +**MCP Server** (future): + +- Expose structured API for querying release state +- Can be called from Claude Code, CLI, or CI/CD +- Build on top of existing `loadReleaseCandidates()` function + +## Questions and Decisions + +### Open Questions + +None at this time - all clarifications provided inline. + +### Design Decisions + +1. **Version synchronization**: All widgets in aggregator share version (simplifies dependency management) +2. **Changelog aggregation**: Widget changelogs copied into aggregator changelog (single source of release notes) +3. **Release from main**: All releases branch from main (simpler workflow, relies on main being stable) +4. **Manual OSS clearance**: Cannot be automated yet (external process) +5. **Draft releases**: Manual approval required (safety gate before publication) +6. **Widget aggregators**: Special case for `charts-web` - widget that bundles other widgets (identified by `changelogType: "module"`) + +## Appendix: Example Scenarios + +### Scenario 1: Release standalone widget with bug fix + +**Initial state**: + +``` +@mendix/carousel-web + version: 2.3.1 + CHANGELOG.md: + ## [Unreleased] + ### Fixed + - We fixed an issue with carousel navigation on mobile devices. +``` + +**Steps**: + +1. Run `rui-prepare-release` +2. Select `@mendix/carousel-web` +3. Choose "patch" bump → `2.3.2` +4. Bump `package.json` and `src/package.xml` to `2.3.2` +5. Create branch `release/carousel-web-v2.3.2` +6. Commit and push +7. GitHub workflow updates changelog, creates draft release +8. Team approves and publishes + +**Final state**: + +``` +@mendix/carousel-web + version: 2.3.2 + CHANGELOG.md: + ## [Unreleased] + + ## [2.3.2] - 2026-04-15 + ### Fixed + - We fixed an issue with carousel navigation on mobile devices. +``` + +### Scenario 2: Release module aggregator with multiple widget changes + +**Initial state**: + +``` +@mendix/data-widgets (module) + version: 3.9.0 + dependencies: [@mendix/datagrid-web, @mendix/gallery-web, @mendix/dropdown-sort-web] + CHANGELOG.md: + ## [Unreleased] + +@mendix/datagrid-web + version: 3.9.0 + CHANGELOG.md: + ## [Unreleased] + ### Fixed + - We fixed an issue with column sorting. + +@mendix/gallery-web + version: 3.9.0 + CHANGELOG.md: + ## [Unreleased] + ### Added + - We added support for lazy loading images. + +@mendix/dropdown-sort-web + version: 3.9.0 + CHANGELOG.md: + ## [Unreleased] + (empty - no changes) +``` + +**Steps**: + +1. Run `rui-prepare-release` +2. Select `@mendix/data-widgets` +3. Choose "minor" bump → `3.10.0` +4. Bump all package.json and package.xml files to `3.10.0`: + - `packages/modules/data-widgets/package.json` + - `packages/pluggableWidgets/datagrid-web/package.json` + `src/package.xml` + - `packages/pluggableWidgets/gallery-web/package.json` + `src/package.xml` + - `packages/pluggableWidgets/dropdown-sort-web/package.json` + `src/package.xml` +5. Create branch `release/data-widgets-v3.10.0` +6. Commit and push +7. GitHub workflow: + - Aggregates widget changelogs into module changelog + - Updates all CHANGELOG.md files + - Creates draft release + +**Final state**: + +``` +@mendix/data-widgets (module) + version: 3.10.0 + CHANGELOG.md: + ## [Unreleased] + + ## [3.10.0] DataWidgets - 2026-04-15 + + ### [3.10.0] Datagrid + #### Fixed + - We fixed an issue with column sorting. + + ### [3.10.0] Gallery + #### Added + - We added support for lazy loading images. + +@mendix/datagrid-web + version: 3.10.0 + CHANGELOG.md: + ## [Unreleased] + + ## [3.10.0] - 2026-04-15 + ### Fixed + - We fixed an issue with column sorting. + +@mendix/gallery-web + version: 3.10.0 + CHANGELOG.md: + ## [Unreleased] + + ## [3.10.0] - 2026-04-15 + ### Added + - We added support for lazy loading images. + +@mendix/dropdown-sort-web + version: 3.10.0 + CHANGELOG.md: + ## [Unreleased] + + ## [3.10.0] - 2026-04-15 + (empty release - version bump only) +``` + +Note: `dropdown-sort-web` gets version bump even though it had no code changes. diff --git a/docs/release-process/release-info-tool.md b/docs/release-process/release-info-tool.md new file mode 100644 index 0000000000..ea4544a86b --- /dev/null +++ b/docs/release-process/release-info-tool.md @@ -0,0 +1,166 @@ +# Release Info Tool + +## Purpose + +The `rui-release-info` tool scans the Mendix web widgets monorepo and outputs structured JSON about packages ready for release. This document explains how to use it. + +## Quick Start + +```bash +# Get packages with unreleased changes (release candidates) +pnpm exec ts-node automation/utils/bin/rui-release-info.ts --candidates + +# Get summary with counts +pnpm exec ts-node automation/utils/bin/rui-release-info.ts --summary + +# Get all packages (including those without changes) +pnpm exec ts-node automation/utils/bin/rui-release-info.ts --all +``` + +## Output Structure + +### ReleaseCandidate Type + +```typescript +interface ReleaseCandidate { + packageType: "widget" | "module"; // From package.json mxpackage.type + hasDependencies: boolean; // Does it bundle other widgets? + name: string; // NPM package name + path: string; // Filesystem path + currentVersion: string; // Current version (e.g., "3.9.0") + appNumber: number; // Mendix Marketplace app number + appName: string; // Display name in Marketplace + hasUnreleasedChanges: boolean; // Does the package itself have changes? + unreleasedEntries: ChangelogEntry[]; // Changelog entries for this package + dependentWidgets?: DependentWidgetInfo[]; // Only present if hasDependencies is true +} + +interface ChangelogEntry { + type: "Fixed" | "Added" | "Changed" | "Removed" | "Breaking changes"; + description: string; +} + +interface DependentWidgetInfo { + name: string; + path: string; + currentVersion: string; + appName: string; + hasUnreleasedChanges: boolean; + unreleasedEntries: ChangelogEntry[]; +} +``` + +## Available Commands + +| Command | Output | Use Case | +| ------------------- | --------------------------------------------------- | --------------------------------------- | +| `--candidates` `-c` | Full JSON array of packages with unreleased changes | Planning releases, reviewing changelogs | +| `--all` `-a` | Full JSON array of ALL packages | Understanding repo structure | +| `--summary` `-s` | Statistics + basic package list | Quick overview | +| `--help` `-h` | Help text | Command reference | + +## Package Types + +Packages are described by two properties: + +- **`packageType`**: Either `"widget"` or `"module"` +- **`hasDependencies`**: Either `true` (bundles other widgets) or `false` (standalone) + +This creates four combinations: + +| packageType | hasDependencies | Description | Examples | +| ----------- | --------------- | ------------------------------------------ | ----------------------------------------------------- | +| `"widget"` | `false` | Standalone widget | `@mendix/carousel-web`, `@mendix/document-viewer-web` | +| `"widget"` | `true` | Widget that bundles other widgets | `@mendix/charts-web` (only example) | +| `"module"` | `false` | Standalone module (no widget dependencies) | `@mendix/web-actions` (JavaScript actions only) | +| `"module"` | `true` | Module that bundles widgets | `@mendix/data-widgets` (Data Grid 2 + filters) | + +### Dependent Widgets + +Widgets that are bundled by packages with `hasDependencies: true` don't have their own Marketplace app number. They appear in the `dependentWidgets` array of their parent package, not as top-level candidates. + +**Example**: `@mendix/datagrid-web` is part of `@mendix/data-widgets` module + +### Version Synchronization + +When a package with `hasDependencies: true` is released, **all its dependent widgets get the same version**, even if they have no code changes. + +## Release Rules + +### When to Release + +A package is a **release candidate** if: + +1. **Independent package** (widget or module): `hasUnreleasedChanges: true` +2. **Aggregator** (widget or module): The package itself OR any dependent widget has `hasUnreleasedChanges: true` + +### Version Synchronization + +When an aggregator is released, **all dependent widgets get the same version** even if they have no changes. + +Example: If `@mendix/data-widgets` releases v3.10.0: + +- All 9 widgets in the bundle → v3.10.0 +- Even widgets without code changes get the version bump + +## Example Output + +```json +[ + { + "packageType": "widget", + "hasDependencies": false, + "name": "@mendix/document-viewer-web", + "currentVersion": "1.2.0", + "appNumber": 240853, + "appName": "Document Viewer", + "hasUnreleasedChanges": true, + "unreleasedEntries": [ + { + "type": "Changed", + "description": "We changed the internal structure of the widget" + } + ] + }, + { + "packageType": "module", + "hasDependencies": true, + "name": "@mendix/data-widgets", + "currentVersion": "3.9.0", + "appNumber": 116540, + "appName": "Data Widgets", + "hasUnreleasedChanges": false, + "unreleasedEntries": [], + "dependentWidgets": [ + { + "name": "@mendix/datagrid-date-filter-web", + "currentVersion": "3.9.0", + "appName": "Date Filter", + "hasUnreleasedChanges": true, + "unreleasedEntries": [ + { + "type": "Fixed", + "description": "We fixed an issue with filter selector dropdown not choosing the best placement on small viewports." + } + ] + } + ] + } +] +``` + +## Testing + +```bash +# Verify tool works +cd /path/to/web-widgets +pnpm exec ts-node automation/utils/bin/rui-release-info.ts --summary + +# Pipe to jq for filtering +pnpm exec ts-node automation/utils/bin/rui-release-info.ts --candidates | \ + jq '.[] | select(.packageType == "module")' + +# Count packages by type +pnpm exec ts-node automation/utils/bin/rui-release-info.ts --summary | \ + jq '{widgets: .independentWidgets, modules: .independentModules, aggregators: (.widgetAggregators + .moduleAggregators)}' +``` diff --git a/docs/release-process/release-workflow.md b/docs/release-process/release-workflow.md new file mode 100644 index 0000000000..e33391566d --- /dev/null +++ b/docs/release-process/release-workflow.md @@ -0,0 +1,379 @@ +# Release Workflow Documentation + +## Overview + +This document describes the release process for Mendix web widgets and modules in the monorepo. + +**Package types**: + +- **Widgets** - UI components +- **Modules** - Bundles of widgets, JavaScript actions, and Mendix documents like Nanoflows, Pages as well as Entities in domain model. +- **Dependent widgets** - Widgets without Marketplace app numbers, bundled by other modules or widgets + +**Dependency structure**: + +- Every module or widget can contain other widgets (dependent widgets). Most of the modules do, most of the widgets don't. +- There is maximum one nesting level (widget → widget or module → widget). + +## Package Types + +### Widgets + +**Location**: `packages/pluggableWidgets/*/` + +**Characteristics**: + +- Has `marketplace.appNumber` field in `package.json` +- Released independently +- Own version tracking (semantic versioning) +- Own changelog lifecycle + +**Example**: `@mendix/carousel-web` + +```json +{ + "name": "@mendix/carousel-web", + "version": "2.3.2", + "mxpackage": { + "type": "widget" + }, + "marketplace": { + "appNumber": 47784, + "appName": "Carousel" + } +} +``` + +**Special case** + +- Widget `@mendix/charts-web` contains other widgets as if it is a module. +- Referencing dependencies, versioning and changelog structure works the same way as for module → widget dependency +- Should be still be referred as normal widget, no extra treatment (dependencies is only an extra step in build process) + +### Modules + +**Location**: `packages/modules/*/` + +**Characteristics**: + +- Has `mxpackage.type: "module"` in `package.json` +- Has `marketplace.appNumber` (published to marketplace) +- Lists dependent widgets in `mxpackage.dependencies` array, might be empty +- Has own CHANGELOG.md entries and additionally aggregates widget changelogs +- Contain other Mendix elements in addition to widgets: JS actions, nanoflows, pages, entities, etc. Those are not referenced in package.json + +**Example**: `@mendix/data-widgets` + +```json +{ + "name": "@mendix/data-widgets", + "version": "3.9.0", + "mxpackage": { + "type": "module", + "dependencies": [ + "@mendix/datagrid-web", + "@mendix/datagrid-date-filter-web", + "@mendix/gallery-web" + // ... more widgets + ] + }, + "marketplace": { + "appNumber": 116540, + "appName": "Data Widgets" + } +} +``` + +### Dependent Widgets + +**Location**: `packages/pluggableWidgets/*/` +**Not returned as top-level release candidates** - appear in `dependentWidgets` array + +**Characteristics**: + +- Does NOT have `marketplace.appNumber` in `package.json` +- Listed in parent's `mxpackage.dependencies` array +- Released only as part of their parent +- Version is set to match the parent version on each release cycle +- Has own CHANGELOG.md (aggregated into parent changelog on release) + +**Example**: `@mendix/datagrid-web` + +```json +{ + "name": "@mendix/datagrid-web", + "version": "3.9.0", + "mxpackage": { + "type": "widget" + }, + "marketplace": { + "appName": "Data Grid 2" + // Note: no appNumber + } +} +``` + +## Version Management + +### Packages without dependencies + +- **Version tracking**: Each package has its own independent version +- **Semantic versioning rules**: + - **Patch**: Bug fixes, small improvements + - **Minor**: New features, backward-compatible changes + - **Major**: Breaking changes, major rewrites + +### Packages with dependencies + +- **Version tracking**: Parent package and all dependent widgets share the same version +- When parent releases version `3.8.0`, ALL dependent widgets become `3.8.0` +- When parent releases `3.8.1` for a single widget fix, ALL widgets still bump to `3.8.1` +- Even if a widget's code didn't change, it gets the version bump + +**Example**: If `@mendix/data-widgets` (module) releases `3.9.0`, then: + +- `@mendix/datagrid-web` → `3.9.0` +- `@mendix/gallery-web` → `3.9.0` +- `@mendix/dropdown-sort-web` → `3.9.0` +- ... all widgets in the module → `3.9.0` + +## Changelog Management + +### Format + +All changelogs follow the [Keep a Changelog](https://keepachangelog.com/) format with Mendix-specific extensions. + +**Standard sections**: + +- `## [Unreleased]` - Unreleased changes +- `## [X.Y.Z] - YYYY-MM-DD` - Released versions + +**Change categories**: + +- `### Fixed` - Bug fixes +- `### Added` - New features +- `### Changed` - Changes to existing functionality +- `### Removed` - Removed features + +### Workflow + +1. **During development**: Developer adds entries under `## [Unreleased]` section +2. **On merge to main**: Changes merged with unreleased changelog entries (no version bump yet) +3. **Release decision**: Team decides to release based on: + - Unreleased changes exist + - Jira story is complete + - Team decision (may wait to bundle multiple stories) +4. **On release**: GitHub workflow moves unreleased entries to new version section + +### Widget Changelogs + +**Location**: `packages/pluggableWidgets/*/CHANGELOG.md` + +**Format** (for widget `@mendix/datagrid-web`): + +```markdown +# Changelog + +All notable changes to this widget will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [3.9.0] - 2026-03-23 + +### Changed + +- We improved accessibility on column selector, added aria-attributes and changed the role to 'menuitemcheckbox'. + +### Added + +- We added a new `Loaded rows` attribute that reflects the number of rows currently loaded for virtual scrolling and load-more pagination modes. + +### Fixed + +- We fixed an issue with Data export crashing on some Android devices. +``` + +### Module Changelogs + +**Location**: `packages/modules/*/CHANGELOG.md` + +**Format** (for module `@mendix/data-widgets`): + +```markdown +# Changelog + +All notable changes to this widget will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [3.9.0] DataWidgets - 2026-03-23 + +### [3.9.0] DatagridDropdownFilter + +#### Fixed + +- We fixed an issue with Dropdown filter captions not updating properly when their template parameters change. + +### [3.9.0] Datagrid + +#### Changed + +- We improved accessibility on column selector, added aria-attributes and changed the role to 'menuitemcheckbox'. + +#### Added + +- We added a new `Loaded rows` attribute that reflects the number of rows currently loaded for virtual scrolling and load-more pagination modes. + +#### Fixed + +- We fixed an issue with Data export crashing on some Android devices. + +### [3.9.0] Gallery + +#### Fixed + +- We fixed the pagination properties `Page attribute`, `Page size attribute`, and `Total count` not being shown in Studio Pro for Virtual Scrolling and Load More pagination modes. +``` + +**Key differences**: + +- Module name in version header: `[3.9.0] DataWidgets - 2026-03-23` +- Subcomponent sections for each widget: `### [3.9.0] Datagrid` +- Aggregates all widget changelogs into single module changelog +- Generated automatically by GitHub workflow during release — the workflow reads each dependent widget's flat `## [Unreleased]` entries and transforms them into the nested `### [X.Y.Z] WidgetName` format shown above + +## Release Preparation Process + +### Prerequisites + +- Changes merged to `main` branch +- Unreleased entries in CHANGELOG.md + +### Steps + +#### 1. Determine what to release + +**Packages without dependencies**: + +- Check if package has unreleased changelog entries +- If so → is releasable + +**Packages with dependencies**: + +- Check if package itself has unreleased changelog entries OR +- Check if any dependent widget has unreleased changelog entries +- If so → is releasable + +#### 2. Determine version bump type + +**Manual decision** (currently): + +- **Patch** (X.Y.Z+1): Bug fixes, small changes +- **Minor** (X.Y+1.0): New features, backward-compatible +- **Major** (X+1.0.0): Breaking changes, major rewrites + +**Future**: Agent should advise this based on changelog entries + +#### 3. Bump versions + +**For packages without dependencies**: + +``` +package.json: version → X.Y.Z +src/package.xml: version → X.Y.Z (widgets only, modules don't have this) +``` + +**For packages with dependencies**: + +``` +# Package itself +packages/{modules|pluggableWidgets}/PACKAGE_NAME/package.json: version → X.Y.Z +packages/pluggableWidgets/PACKAGE_NAME/src/package.xml: version → X.Y.Z (widgets only) + +# All dependent widgets +packages/pluggableWidgets/DEPENDENT_WIDGET_1/package.json: version → X.Y.Z +packages/pluggableWidgets/DEPENDENT_WIDGET_1/src/package.xml: version → X.Y.Z +packages/pluggableWidgets/DEPENDENT_WIDGET_2/package.json: version → X.Y.Z +packages/pluggableWidgets/DEPENDENT_WIDGET_2/src/package.xml: version → X.Y.Z +... (all dependent widgets) +``` + +**Important**: Only version is bumped at this moment. Changelog files themselves are updated on step 6. + +#### 4. Create release branch + +Create temporary branch: + +``` +tmp/PACKAGE_NAME-vX.Y.Z +``` + +Example: `tmp/carousel-web-v2.3.2` or `tmp/data-widgets-v3.9.0` + +#### 5. Commit and push + +Commit message format: + +``` +chore(PACKAGE_NAME): bump version to X.Y.Z +``` + +Push branch to GitHub + +#### 6. Trigger GitHub release workflow + +The GitHub workflow (`Release` workflow) is triggered manually via UI or via a script. + +What it does: + +1. Reads the version from package.json +2. Updates CHANGELOG.md: + - Moves all entries from `## [Unreleased]` section to a new section for the new release `## [X.Y.Z] - YYYY-MM-DD` + - Unreleased section becomes empty +3. For packages with dependencies: aggregates widget changelogs into parent's changelog +4. Builds artifacts and creates a draft GitHub release based on them +5. Commits changelog updates to `tmp/PACKAGE_NAME-vX.Y.Z` +6. Opens PR to `main` containing changes from `tmp/PACKAGE_NAME-vX.Y.Z` + +#### 7. Create Jira version entries + +By team member via Jira UI or via helper scripts. + +**Manual step**: + +- In Jira a new Release is created with the name `PACKAGE_NAME-vX.Y.Z`. +- This release is then attached to the Jira story (or stories if new version contains multiple work items) + +#### 8. OSS Clearance + +**Manual step**: + +- When the draft release created by workflow on step 6 is ready. +- Team member requests OSS clearance based on draft release artifacts +- Uses scripts in `automation/utils/bin/rui-oss-clearance.ts` +- When OSS clearance is complete team member uploads clearance artifact (HTML file) to the draft release. + +This step has to be finished before the next steps. + +#### 9. Approve and publish + +**Manual step**: + +- Team member reviews draft release on GitHub and makes sure release artifacts and oss clearance artifact (HTML file) are present +- If comfortable with release they "Publish" the release +- Artifacts are uploaded to Mendix Marketplace by a separate workflow + +#### 10. Merging changelogs and completion + +Team members approve and merge PR opened at step 6. +After merge `main` contains correct changelogs +Branch `tmp/PACKAGE_NAME-vX.Y.Z` is removed manually + +#### 11. Completion in Jira + +At this stage the release process is considered complete. +Relevant Jira stories are marked as Done and relevant Jira releases is marked as released.