From c6c64c83a3fcc3e4670cde8e121b32a995c5da1f Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 16 Apr 2026 15:58:16 +0200 Subject: [PATCH 1/2] chore: add release info code and cli tool to get releasable widgets/modules --- automation/utils/bin/rui-release-info.ts | 67 ++ automation/utils/package.json | 1 + automation/utils/src/index.ts | 2 + automation/utils/src/io/filesystem.ts | 43 ++ automation/utils/src/release-candidates.ts | 296 +++++++++ docs/release-process/release-info-tool.md | 166 +++++ docs/release-process/release-workflow.md | 732 +++++++++++++++++++++ 7 files changed, 1307 insertions(+) create mode 100755 automation/utils/bin/rui-release-info.ts create mode 100644 automation/utils/src/io/filesystem.ts create mode 100644 automation/utils/src/release-candidates.ts create mode 100644 docs/release-process/release-info-tool.md create mode 100644 docs/release-process/release-workflow.md 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.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..d28f4c84b8 --- /dev/null +++ b/docs/release-process/release-workflow.md @@ -0,0 +1,732 @@ +# Release Workflow Documentation + +## Overview + +This document describes the release process for Mendix web widgets and modules in the monorepo. + +**Package types**: + +- **Widgets** (`packageType: "widget"`) - UI components +- **Modules** (`packageType: "module"`) - Bundles of widgets and/or JavaScript actions + +**Dependency structure**: + +- **Standalone** (`hasDependencies: false`) - Released independently +- **Aggregators** (`hasDependencies: true`) - Bundle other widgets, all share the same version +- **Dependent widgets** - Widgets without Marketplace app numbers, bundled by aggregators + +## Package Structure + +### Standalone Widgets + +**Location**: `packages/pluggableWidgets/*/` +**Properties**: `packageType: "widget"`, `hasDependencies: false` + +**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" + } +} +``` + +### Standalone Modules + +**Location**: `packages/modules/*/` +**Properties**: `packageType: "module"`, `hasDependencies: false` + +**Characteristics**: + +- Has `marketplace.appNumber` +- No widget dependencies (just JavaScript actions or other module content) +- Released independently +- Own version tracking + +**Example**: `@mendix/web-actions` + +```json +{ + "name": "@mendix/web-actions", + "version": "2.0.0", + "mxpackage": { + "type": "module" + }, + "marketplace": { + "appNumber": 114337, + "appName": "Web Actions" + } +} +``` + +### Widget Aggregators + +**Location**: `packages/pluggableWidgets/*/` +**Properties**: `packageType: "widget"`, `hasDependencies: true` + +**Characteristics**: + +- Has `mxpackage.type: "widget"` in `package.json` +- Has `mxpackage.changelogType: "module"` (special flag for changelog aggregation) +- Has `marketplace.appNumber` (published to marketplace) +- Lists dependent widgets in `mxpackage.dependencies` array +- Has own CHANGELOG.md that aggregates widget changelogs + +**Example**: `@mendix/charts-web` (the only widget-to-widget aggregator) + +```json +{ + "name": "@mendix/charts-web", + "version": "6.3.0", + "mxpackage": { + "type": "widget", + "changelogType": "module", + "dependencies": [ + "@mendix/area-chart-web", + "@mendix/bar-chart-web", + "@mendix/line-chart-web", + "@mendix/pie-doughnut-chart-web" + // ... more chart widgets + ] + }, + "marketplace": { + "appNumber": 105695, + "appName": "Charts" + } +} +``` + +### Module Aggregators + +**Location**: `packages/modules/*/` +**Properties**: `packageType: "module"`, `hasDependencies: true` + +**Characteristics**: + +- Has `mxpackage.type: "module"` in `package.json` +- Has `marketplace.appNumber` (published to marketplace) +- Lists dependent widgets in `mxpackage.dependencies` array +- Has own CHANGELOG.md that aggregates widget changelogs +- May contain JS actions in addition to widgets + +**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 an aggregator's `mxpackage.dependencies` array +- Released only as part of their parent aggregator +- Version always matches parent aggregator version +- Has own CHANGELOG.md (aggregated into aggregator 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 + +### Standalone Packages (hasDependencies: false) + +- **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 + +### Aggregator Packages (hasDependencies: true) + +- **Version tracking**: Aggregator and all dependent widgets share the same version +- When aggregator releases version `3.8.0`, ALL dependent widgets become `3.8.0` +- When aggregator 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 aggregator) 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 + +## Release Preparation Process + +### Prerequisites + +- Changes merged to `main` branch +- Unreleased entries in CHANGELOG.md +- Jira story complete (or team decision to release) +- GitHub authentication configured (`gh` CLI) +- Jira authentication configured (optional) + +### Steps + +The release is prepared using the `rui-prepare-release` script: + +#### 1. Determine what to release + +**For standalone packages** (`hasDependencies: false`): + +- Check if package has unreleased changelog entries +- Package can be released independently + +**For aggregators** (`hasDependencies: true`): + +- Check if aggregator has unreleased changelog entries OR +- Check if any dependent widget has unreleased changelog entries +- If either has changes → aggregator 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 determine this based on changelog entry types + +#### 3. Bump versions + +**For standalone packages** (`hasDependencies: false`): + +``` +package.json: version → X.Y.Z +src/package.xml: version → X.Y.Z (widgets only, modules don't have this) +``` + +**For aggregators** (`hasDependencies: true`): + +``` +# Aggregator package +packages/{modules|pluggableWidgets}/AGGREGATOR_NAME/package.json: version → X.Y.Z +packages/pluggableWidgets/AGGREGATOR_NAME/src/package.xml: version → X.Y.Z (widget aggregators only) + +# All dependent widgets +packages/pluggableWidgets/WIDGET_1/package.json: version → X.Y.Z +packages/pluggableWidgets/WIDGET_1/src/package.xml: version → X.Y.Z +packages/pluggableWidgets/WIDGET_2/package.json: version → X.Y.Z +packages/pluggableWidgets/WIDGET_2/src/package.xml: version → X.Y.Z +... (all dependent widgets) +``` + +**Note**: + +- Module aggregators (`packageType: "module"`, `hasDependencies: true`): package.json in `packages/modules/`, no package.xml +- Widget aggregators (`packageType: "widget"`, `hasDependencies: true`): package.json in `packages/pluggableWidgets/`, has package.xml +- Standalone modules (`packageType: "module"`, `hasDependencies: false`): package.json in `packages/modules/`, no package.xml +- Standalone widgets (`packageType: "widget"`, `hasDependencies: false`): package.json in `packages/pluggableWidgets/`, has package.xml + +**Important**: Changelog files are NOT modified at this stage + +#### 4. Create release branch + +Create temporary branch: + +``` +release/PACKAGE_NAME-vX.Y.Z +``` + +Example: `release/carousel-web-v2.3.2` or `release/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) does: + +1. Reads the version from package.json +2. Updates CHANGELOG.md: + - Moves `## [Unreleased]` entries to `## [X.Y.Z] - YYYY-MM-DD` + - Clears Unreleased section +3. For aggregators: aggregates widget changelogs into aggregator changelog +4. Creates a draft GitHub release +5. Builds artifacts (MPK files) + +#### 7. OSS Clearance + +**Manual step** (not automatable yet): + +- Team member requests OSS clearance +- Uses scripts in `automation/utils/bin/rui-oss-clearance.ts` + +#### 8. Approve and publish + +**Manual step**: + +- Team member reviews draft release on GitHub +- Approves and publishes the release +- Artifacts are uploaded to Mendix Marketplace + +## Determining Releasability + +### Data Structure for Release Candidates + +See `docs/requirements/release-info-tool-summary.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. From 7f0079058f153434855192234ac661918c8f958a Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 17 Apr 2026 12:13:38 +0200 Subject: [PATCH 2/2] chore: update release workflow description --- .../release-info-tool-usage.md | 306 +++++++++++ docs/release-process/release-workflow.md | 509 +++--------------- 2 files changed, 384 insertions(+), 431 deletions(-) create mode 100644 docs/release-process/release-info-tool-usage.md 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-workflow.md b/docs/release-process/release-workflow.md index d28f4c84b8..e33391566d 100644 --- a/docs/release-process/release-workflow.md +++ b/docs/release-process/release-workflow.md @@ -6,21 +6,20 @@ This document describes the release process for Mendix web widgets and modules i **Package types**: -- **Widgets** (`packageType: "widget"`) - UI components -- **Modules** (`packageType: "module"`) - Bundles of widgets and/or JavaScript actions +- **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**: -- **Standalone** (`hasDependencies: false`) - Released independently -- **Aggregators** (`hasDependencies: true`) - Bundle other widgets, all share the same version -- **Dependent widgets** - Widgets without Marketplace app numbers, bundled by aggregators +- 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 Structure +## Package Types -### Standalone Widgets +### Widgets -**Location**: `packages/pluggableWidgets/*/` -**Properties**: `packageType: "widget"`, `hasDependencies: false` +**Location**: `packages/pluggableWidgets/*/` **Characteristics**: @@ -45,83 +44,23 @@ This document describes the release process for Mendix web widgets and modules i } ``` -### Standalone Modules - -**Location**: `packages/modules/*/` -**Properties**: `packageType: "module"`, `hasDependencies: false` - -**Characteristics**: - -- Has `marketplace.appNumber` -- No widget dependencies (just JavaScript actions or other module content) -- Released independently -- Own version tracking - -**Example**: `@mendix/web-actions` +**Special case** -```json -{ - "name": "@mendix/web-actions", - "version": "2.0.0", - "mxpackage": { - "type": "module" - }, - "marketplace": { - "appNumber": 114337, - "appName": "Web Actions" - } -} -``` +- 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) -### Widget Aggregators +### Modules -**Location**: `packages/pluggableWidgets/*/` -**Properties**: `packageType: "widget"`, `hasDependencies: true` - -**Characteristics**: - -- Has `mxpackage.type: "widget"` in `package.json` -- Has `mxpackage.changelogType: "module"` (special flag for changelog aggregation) -- Has `marketplace.appNumber` (published to marketplace) -- Lists dependent widgets in `mxpackage.dependencies` array -- Has own CHANGELOG.md that aggregates widget changelogs - -**Example**: `@mendix/charts-web` (the only widget-to-widget aggregator) - -```json -{ - "name": "@mendix/charts-web", - "version": "6.3.0", - "mxpackage": { - "type": "widget", - "changelogType": "module", - "dependencies": [ - "@mendix/area-chart-web", - "@mendix/bar-chart-web", - "@mendix/line-chart-web", - "@mendix/pie-doughnut-chart-web" - // ... more chart widgets - ] - }, - "marketplace": { - "appNumber": 105695, - "appName": "Charts" - } -} -``` - -### Module Aggregators - -**Location**: `packages/modules/*/` -**Properties**: `packageType: "module"`, `hasDependencies: true` +**Location**: `packages/modules/*/` **Characteristics**: - Has `mxpackage.type: "module"` in `package.json` - Has `marketplace.appNumber` (published to marketplace) -- Lists dependent widgets in `mxpackage.dependencies` array -- Has own CHANGELOG.md that aggregates widget changelogs -- May contain JS actions in addition to widgets +- 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` @@ -153,10 +92,10 @@ This document describes the release process for Mendix web widgets and modules i **Characteristics**: - Does NOT have `marketplace.appNumber` in `package.json` -- Listed in an aggregator's `mxpackage.dependencies` array -- Released only as part of their parent aggregator -- Version always matches parent aggregator version -- Has own CHANGELOG.md (aggregated into aggregator changelog on release) +- 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` @@ -176,7 +115,7 @@ This document describes the release process for Mendix web widgets and modules i ## Version Management -### Standalone Packages (hasDependencies: false) +### Packages without dependencies - **Version tracking**: Each package has its own independent version - **Semantic versioning rules**: @@ -184,14 +123,14 @@ This document describes the release process for Mendix web widgets and modules i - **Minor**: New features, backward-compatible changes - **Major**: Breaking changes, major rewrites -### Aggregator Packages (hasDependencies: true) +### Packages with dependencies -- **Version tracking**: Aggregator and all dependent widgets share the same version -- When aggregator releases version `3.8.0`, ALL dependent widgets become `3.8.0` -- When aggregator releases `3.8.1` for a single widget fix, ALL widgets still bump to `3.8.1` +- **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 aggregator) releases `3.9.0`, then: +**Example**: If `@mendix/data-widgets` (module) releases `3.9.0`, then: - `@mendix/datagrid-web` → `3.9.0` - `@mendix/gallery-web` → `3.9.0` @@ -305,7 +244,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - 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 +- 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 @@ -313,26 +252,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Changes merged to `main` branch - Unreleased entries in CHANGELOG.md -- Jira story complete (or team decision to release) -- GitHub authentication configured (`gh` CLI) -- Jira authentication configured (optional) ### Steps -The release is prepared using the `rui-prepare-release` script: - #### 1. Determine what to release -**For standalone packages** (`hasDependencies: false`): +**Packages without dependencies**: - Check if package has unreleased changelog entries -- Package can be released independently +- If so → is releasable -**For aggregators** (`hasDependencies: true`): +**Packages with dependencies**: -- Check if aggregator has unreleased changelog entries OR +- Check if package itself has unreleased changelog entries OR - Check if any dependent widget has unreleased changelog entries -- If either has changes → aggregator is releasable +- If so → is releasable #### 2. Determine version bump type @@ -342,50 +276,43 @@ The release is prepared using the `rui-prepare-release` script: - **Minor** (X.Y+1.0): New features, backward-compatible - **Major** (X+1.0.0): Breaking changes, major rewrites -**Future**: Agent should determine this based on changelog entry types +**Future**: Agent should advise this based on changelog entries #### 3. Bump versions -**For standalone packages** (`hasDependencies: false`): +**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 aggregators** (`hasDependencies: true`): +**For packages with dependencies**: ``` -# Aggregator package -packages/{modules|pluggableWidgets}/AGGREGATOR_NAME/package.json: version → X.Y.Z -packages/pluggableWidgets/AGGREGATOR_NAME/src/package.xml: version → X.Y.Z (widget aggregators only) +# 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/WIDGET_1/package.json: version → X.Y.Z -packages/pluggableWidgets/WIDGET_1/src/package.xml: version → X.Y.Z -packages/pluggableWidgets/WIDGET_2/package.json: version → X.Y.Z -packages/pluggableWidgets/WIDGET_2/src/package.xml: version → X.Y.Z +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) ``` -**Note**: - -- Module aggregators (`packageType: "module"`, `hasDependencies: true`): package.json in `packages/modules/`, no package.xml -- Widget aggregators (`packageType: "widget"`, `hasDependencies: true`): package.json in `packages/pluggableWidgets/`, has package.xml -- Standalone modules (`packageType: "module"`, `hasDependencies: false`): package.json in `packages/modules/`, no package.xml -- Standalone widgets (`packageType: "widget"`, `hasDependencies: false`): package.json in `packages/pluggableWidgets/`, has package.xml - -**Important**: Changelog files are NOT modified at this stage +**Important**: Only version is bumped at this moment. Changelog files themselves are updated on step 6. #### 4. Create release branch Create temporary branch: ``` -release/PACKAGE_NAME-vX.Y.Z +tmp/PACKAGE_NAME-vX.Y.Z ``` -Example: `release/carousel-web-v2.3.2` or `release/data-widgets-v3.9.0` +Example: `tmp/carousel-web-v2.3.2` or `tmp/data-widgets-v3.9.0` #### 5. Commit and push @@ -399,334 +326,54 @@ Push branch to GitHub #### 6. Trigger GitHub release workflow -The GitHub workflow (`Release` workflow) does: +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 `## [Unreleased]` entries to `## [X.Y.Z] - YYYY-MM-DD` - - Clears Unreleased section -3. For aggregators: aggregates widget changelogs into aggregator changelog -4. Creates a draft GitHub release -5. Builds artifacts (MPK files) + - 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. OSS Clearance +#### 7. Create Jira version entries -**Manual step** (not automatable yet): - -- Team member requests OSS clearance -- Uses scripts in `automation/utils/bin/rui-oss-clearance.ts` - -#### 8. Approve and publish +By team member via Jira UI or via helper scripts. **Manual step**: -- Team member reviews draft release on GitHub -- Approves and publishes the release -- Artifacts are uploaded to Mendix Marketplace - -## Determining Releasability - -### Data Structure for Release Candidates - -See `docs/requirements/release-info-tool-summary.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 -``` +- 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) -**Logic**: +#### 8. OSS Clearance -``` -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. -``` +**Manual step**: -**Steps**: +- 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. -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 +This step has to be finished before the next steps. -**Final state**: +#### 9. Approve and publish -``` -@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 +**Manual step**: -**Initial state**: +- 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 -``` -@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) -``` +#### 10. Merging changelogs and completion -**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**: +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 -``` -@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) -``` +#### 11. Completion in Jira -Note: `dropdown-sort-web` gets version bump even though it had no code changes. +At this stage the release process is considered complete. +Relevant Jira stories are marked as Done and relevant Jira releases is marked as released.