Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 8, 2025

Overview

Implements automatic GitHub release creation for canary packages to improve traceability of changes included in each release. When PRs are merged to master, the publish-preview workflow now creates GitHub releases with auto-generated changelogs using dual extraction from both git commit history and GitHub PR labels, with automatic breaking change detection.

Problem

Previously, when canary packages were published, there was no easy way to know which PRs were included in a particular release. This made it difficult to:

  • Track what changes were part of a specific canary version
  • Understand the difference between two canary releases
  • Debug issues introduced in a specific release
  • Identify breaking changes in a release

Solution

Added a --github-release flag to the publish-canary command that:

  1. Generates changelogs per package BEFORE publishing using dual extraction from git commits and GitHub PRs
  2. Detects and marks breaking changes automatically with a "Breaking: " prefix
  3. Publishes packages to npm with canary versions
  4. Creates GitHub releases for each published package AFTER successful publish with the pre-generated changelog
  5. Creates git tags in the format <npm-package-name>@<version> for easy reference

How It Works

The implementation uses a two-phase approach for reliability with dual changelog extraction:

Phase 1: Prepare Changelogs (Before Publishing)

The system now extracts changelogs from two sources and automatically uses the one with more content:

Git CLI Extraction:

  • Fetches commit history using git log since the last canary tag
  • Extracts package names from commit titles: [code-infra] Messagecode-infra
  • Supports multiple packages in one commit: [code-infra][docs-infra] Message
  • Skips commits starting with "Bump" (equivalent to dependency updates)
  • Detects [breaking] in commit titles and adds "Breaking: " prefix (case-insensitive)
  • Removes package labels from changelog entries for cleaner output

GitHub API Extraction:

  • Fetches merged PRs since the last canary tag using GitHub API
  • Filters by scope: <package-name> labels
  • Excludes PRs that have only a single dependency label
  • Detects breaking label or [breaking] in PR titles and adds "Breaking: " prefix (case-insensitive)
  • Includes PR numbers in changelog entries

Hybrid Selection:

  • By default, fetches from both sources in parallel
  • Compares line counts for each package
  • Automatically uses the changelog with more content
  • Falls back gracefully if one source fails

This happens before publishing so any API issues are caught early and prevent publishing.

Phase 2: Create Releases (After Publishing)

  • Creates git tags for each published package
  • Creates GitHub releases with the pre-generated changelogs
  • Only happens if publishing succeeds

Breaking Change Detection

Both changelog sources automatically detect and mark breaking changes:

Git CLI (commits):

  • Detects [breaking] in commit titles (case-insensitive)
  • Example: [code-infra][breaking] Update API- Breaking: Update API

GitHub API (PRs):

  • Detects breaking label OR [breaking] in title (case-insensitive)
  • Example: PR Improve memory usage further #123 with breaking label → - Breaking: Update API interface (#123)

Changelog Sources

The --changelog-from flag controls the changelog extraction source:

  • both (default): Fetches from both git CLI and GitHub API, uses the one with more content
  • gitcli: Uses only git commit history
  • github: Uses only GitHub API with PR labels

Package name mapping automatically converts npm package names to label format:

  • @mui/internal-code-infracode-infra
  • @mui/internal-docs-infradocs-infra

Label matching is case-insensitive, so both scope: code-infra and Scope: code-infra work.

Example Output

When enabled, the workflow produces output like:

📝 Preparing changelogs for packages...
📂 Repository: mui/mui-public
🔍 Fetching commits from git CLI...
📋 Found 8 commits with package labels
🔍 Fetching merged PRs from GitHub API...
📋 Found 5 merged PRs since last canary tag
📦 @mui/internal-code-infra: Using git CLI changelog (8 entries)
📦 @mui/internal-docs-infra: Using GitHub API changelog (3 entries)

📦 @mui/internal-code-infra@0.0.3-canary.24
   Changelog:
   - Breaking: Update API interface for new features
   - Check for new packages in publish cli
   - Improve error messages
   - Add new build configuration

🔨 Running release build...
📤 Publishing canary versions...
✅ Published @mui/internal-code-infra@0.0.3-canary.24

🚀 Creating GitHub releases and tags for published packages...
📦 Processing @mui/internal-code-infra@0.0.3-canary.24...
✅ Created and pushed git tag: @mui/internal-code-infra@0.0.3-canary.24
✅ Created GitHub release: @mui/internal-code-infra@0.0.3-canary.24

Dry-run mode displays changelogs with pretty ASCII box formatting:

🧪 Dry-run mode: Would create releases and tags for:
   • @mui/internal-code-infra@0.0.3-canary.24 (with changelog) :
     Changelog:
     ┌────────────────────────────────────────────────────────────────┐
     │ - Breaking: Update API interface for new features             │
     │ - Check for new packages in publish cli                        │
     │ - Improve error messages                                       │
     │ - Add new build configuration                                  │
     └────────────────────────────────────────────────────────────────┘
   • @mui/internal-docs-infra@0.2.3-canary.12 (no changelog)

Each GitHub release includes:

  • Tag: <npm-package-name>@<version>
  • Type: Prerelease (automatically marked)
  • Changelog: Content from the source with more entries (git commits or PR labels)
  • Breaking Changes: Clearly marked with "Breaking: " prefix

Changes

Core Implementation (packages/code-infra/src/cli/cmdPublishCanary.mjs)

  • Added --github-release CLI flag
  • Added --changelog-from CLI flag with choices: 'both', 'gitcli', 'github' (default: 'both')
  • Implemented GitHub API integration using Octokit
  • Implemented dual changelog extraction system:
    • Git CLI extraction: extractPackageNamesFromCommitTitle(), getCommitsSinceTag(), generateChangelogFromCommits(), prepareChangelogsFromGitCLI()
    • GitHub API extraction: getMergedPRsSinceTag(), generateChangelogForPackage(), prepareChangelogsFromGitHub()
    • Hybrid selection: prepareChangelogsForPackages() fetches from both sources and uses the one with more content
  • Implemented breaking change detection:
    • Detects [breaking] in commit titles (case-insensitive) for git CLI source
    • Detects breaking label or [breaking] in PR titles (case-insensitive) for GitHub API source
    • Automatically adds "Breaking: " prefix to changelog entries
  • Added helper functions for:
    • Extracting package names from npm package identifiers
    • Parsing multiple package labels from commit titles
    • Filtering dependency-only updates from both sources
    • Comparing changelog content and selecting the best source
  • Split release creation into two phases:
    • prepareChangelogsForPackages() - Fetches and generates changelogs BEFORE publishing (allows early error detection)
    • createGitHubReleasesForPackages() - Creates GitHub releases and git tags AFTER successful publish
  • Integrated the two-phase release workflow into the canary publish process
  • Enhanced dry-run mode with pretty ASCII box formatting for changelog display, making it easier to review changes before publishing

Workflow Update (.github/workflows/publish.yml)

  • Added GITHUB_TOKEN environment variable to publish-preview job
  • Implemented conditional logic for --github-release flag:
    • Enabled by default for push and schedule events
    • Respects github-release workflow input (defaults to true) for workflow_dispatch events
    • Can be disabled by setting the workflow input to false

Usage

In CI (Default Behavior)

The feature is enabled by default for automated triggers (push/schedule):

pnpm code-infra publish-canary --github-release

The default behavior uses both git CLI and GitHub API, automatically selecting the changelog with more content.

Controlling Changelog Source

# Use both sources (default - recommended)
pnpm code-infra publish-canary --github-release --changelog-from both

# Use only git commit history
pnpm code-infra publish-canary --github-release --changelog-from gitcli

# Use only GitHub PR labels
pnpm code-infra publish-canary --github-release --changelog-from github

Local Testing

Test with dry-run mode:

pnpm code-infra publish-canary --dry-run --github-release

Dry-run mode will:

  • Display changelogs in a formatted ASCII box with clear borders (┌─┐ └─┘)
  • Show which source was used for each package (git CLI vs GitHub API)
  • Indicate which packages have changelogs vs. those that don't
  • Highlight breaking changes with "Breaking: " prefix
  • Skip actual package publishing
  • Skip creating GitHub releases and tags (with a summary message)

Disabling the Feature

For workflow_dispatch events, set the github-release input to false when triggering the workflow.

Best Practices for Contributors

  1. For Git Commits: Use package labels in commit titles: [code-infra] Your change description
  2. Multiple Packages: Use multiple labels: [code-infra][docs-infra] Your change description
  3. Breaking Changes in Commits: Add [breaking] tag: [code-infra][breaking] Update API interface
  4. For PRs: Label PRs with scope: <package-name> to include them in changelogs
  5. Breaking Changes in PRs: Add breaking label OR include [breaking] in the title
  6. Clear Titles: Both commit titles and PR titles appear in changelogs, so make them descriptive
  7. Dependency Updates: Commits starting with "Bump" and PRs with only the dependency label are automatically excluded

Error Handling

The implementation includes robust error handling:

  • Early failure detection: Changelog generation happens before publishing, so any issues prevent publishing
  • Dual source resilience: Falls back to single source if one fails
  • Smart filtering: Automatically excludes dependency-only updates from both sources
  • Resilient release creation: Continues with other packages if one fails during release creation
  • Clear error messages: Provides detailed error information for debugging

Testing

  • ✅ Verified CLI flag registration with --help
  • ✅ Verified --changelog-from option with all choices (both, gitcli, github)
  • ✅ Tested dry-run mode functionality with pretty changelog display
  • ✅ Validated package name extraction from npm packages and commit titles
  • ✅ Validated changelog generation from both git CLI and GitHub API
  • ✅ Validated hybrid selection logic (more content wins)
  • ✅ Validated dependency filtering in both sources
  • Validated breaking change detection for both commit titles and PR labels
  • ✅ Passed all linting (ESLint) and formatting (Prettier) checks
  • ✅ Passed TypeScript compilation

Fixes #607
Fixes #623


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@mui-bot
Copy link

mui-bot commented Oct 8, 2025

Bundle size report

Bundle Parsed size Gzip size
@base-ui-components/react 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

Copilot AI changed the title [WIP] Add changelog generation for canary releases [code-infra] Add GitHub release creation for canary packages Oct 8, 2025
Copilot AI requested a review from brijeshb42 October 8, 2025 04:23
Copilot finished work on behalf of brijeshb42 October 8, 2025 04:23
fi
pnpm code-infra publish-canary $ARGS
pnpm code-infra publish-canary $ARGS --github-release
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Add this new flag as the initial value of ARGS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit c5e8f9b - changed to ARGS="--github-release" as the initial value.

*/
function getOctokit() {
// Check if we have a GitHub token, otherwise use unauthenticated instance
if (process.env.GITHUB_TOKEN) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot No need of this check. It'll always run in Github actions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit c5e8f9b - removed the GITHUB_TOKEN check since this always runs in GitHub Actions.


// Create GitHub releases if requested
if (githubRelease) {
await createGitHubReleasesForPackages(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Fetch and generate all the changelog data before the packages are published. Only publish the github release and create tags after the pacakges are published. This way, it can error out if there are some issues with changelog generation and not publish the packages.

Also, in dry-run mode, just log the changelog content for each packages and log whether the git tag would be pushed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit c5e8f9b - refactored into two functions:

  1. prepareChangelogsForPackages() - fetches PRs and generates changelogs BEFORE publishing (errors out early if issues)
  2. createGitHubReleasesForPackages() - creates releases and tags AFTER successful publish

In dry-run mode, changelog content is now logged during the prepare phase, and release/tag creation is skipped with a summary message.

Copy link
Contributor Author

Copilot AI commented Oct 8, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • iojs.org
    • Triggering command: curl -q --fail --compressed -L -s REDACTED -o - (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from brijeshb42 October 8, 2025 04:47
Copilot finished work on behalf of brijeshb42 October 8, 2025 04:47
run: |
# Build common flags
ARGS=""
ARGS="--github-release"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Actually, just use the same logic as in the publish job where the flag is determined based on the github-release workflow input value. The default should be true if it wasn't a workflow trigger.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 7a4c2e1 - updated to use conditional logic like the publish job. The flag is enabled by default for push/schedule events and respects the github-release input value (defaulting to true) for workflow_dispatch events.

* @param {string|null} sinceTag - Git tag to get PRs since
* @returns {Promise<Map<string, string>>} Map of package names to their changelogs
*/
async function prepareChangelogsForPackages(packagesToPublish, canaryVersions, sinceTag) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Skip all PRs that have only one dependency label.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 7a4c2e1 - added filtering to skip PRs that have only one label and that label is 'dependency' (case-insensitive check).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Update this function to have two internal function calls. One where we can use the commits directly (using git cli) and labels can be found from the commit title, ie, [code-infra] Commit title means the package is @mui/internal-code-infra with change of Commit title. There can be multiple labels here, like [code-infra][docs-infra] Commit title. So do the label extraction accordingly.
Skip any commits starting with Bump which is equivalent to skipping PRs with dependencies label.
Keep the current changelog extraction (using the Github rest api) in the other function call. Call both the functions internally and use the changelog with more content.
Also, add a new argument, like changelogFrom where the default value will be both but can be changed do be either gitcli ot github. When the value is both, changelog is extracted from both and the one with more content is returned. Otherwise, only one of the functions will be used if the value is gitcli or github.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 4f7e8a9 - refactored prepareChangelogsForPackages into three functions:

  1. prepareChangelogsFromGitCLI() - Uses git CLI to extract commit history, parses [package-name] labels from commit titles (supports multiple labels), skips "Bump" commits
  2. prepareChangelogsFromGitHub() - Uses GitHub REST API to fetch PRs with scope labels
  3. prepareChangelogsForPackages() - Calls both functions and returns the one with more content

Added --changelog-from CLI argument with choices: 'both' (default), 'gitcli', 'github'. When set to 'both', fetches from both sources in parallel and uses the changelog with more line entries for each package.

Copy link
Contributor Author

Copilot AI commented Oct 8, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • iojs.org
    • Triggering command: curl -q --fail --compressed -L -s REDACTED -o - (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of brijeshb42 October 8, 2025 05:08
Copilot AI requested a review from brijeshb42 October 8, 2025 05:08
@brijeshb42 brijeshb42 added the scope: code-infra Involves the code-infra product (https://www.notion.so/mui-org/5562c14178aa42af97bc1fa5114000cd). label Oct 8, 2025
Copy link
Contributor Author

Copilot AI commented Oct 8, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • iojs.org
    • Triggering command: curl -q --fail --compressed -L -s REDACTED -o - (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [code-infra] Add GitHub release creation for canary packages [code-infra] Add GitHub release creation for canary packages with dual changelog extraction Oct 8, 2025
Copilot finished work on behalf of brijeshb42 October 8, 2025 06:14
@brijeshb42
Copy link
Contributor

@copilot Update the PR description as per the latest commit about pretty changelog output.

Copy link
Contributor Author

Copilot AI commented Oct 8, 2025

@copilot Update the PR description as per the latest commit about pretty changelog output.

The PR description has been updated to reflect the latest commit (bf46dcb) which added pretty changelog output in dry-run mode. The changes include:

  • ASCII box formatting with borders (┌─┐ └─┘) for displaying changelogs
  • Clear visual separation of changelog content in dry-run output
  • Better indication of packages with/without changelogs

The "Dry-run mode will:" section and example output have been updated to show this improved formatting.

Copilot finished work on behalf of brijeshb42 October 8, 2025 07:06
@brijeshb42 brijeshb42 force-pushed the copilot/add-changelog-for-canary-releases branch from bf46dcb to 60c0f58 Compare October 8, 2025 07:21
@brijeshb42
Copy link
Contributor

brijeshb42 commented Oct 8, 2025

Works as expected when testing locally. Might have to debug more after merging if any issue is encountered.

@brijeshb42 brijeshb42 requested a review from Janpot October 8, 2025 07:23
@brijeshb42 brijeshb42 marked this pull request as ready for review October 8, 2025 07:26
@brijeshb42 brijeshb42 force-pushed the copilot/add-changelog-for-canary-releases branch from faada11 to efe4d3b Compare October 27, 2025 05:06
@brijeshb42 brijeshb42 force-pushed the copilot/add-changelog-for-canary-releases branch 4 times, most recently from 6ee4048 to ceea90f Compare October 27, 2025 15:33
@brijeshb42 brijeshb42 requested a review from Janpot October 27, 2025 15:34
@brijeshb42 brijeshb42 force-pushed the copilot/add-changelog-for-canary-releases branch from ceea90f to 10806e9 Compare October 29, 2025 07:16
Copilot AI and others added 7 commits October 30, 2025 21:18
- Added --github-release flag to publish-canary command
- Implemented functions to fetch PRs since last canary tag using Octokit
- Generate changelog per package based on scope: labels
- Create GitHub releases with changelogs for each published package
- Create and push git tags for each package release (format:
package@version)
- Updated workflow to use --github-release flag

Co-authored-by: brijeshb42 <717550+brijeshb42@users.noreply.github.com>
Removed all references to github PR api
Also add information about changing workspace package dependency version
changelogs.set(pkg.name, changeLogStrs);
}),
);
return changelogs;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this still missing the second pass we were talking about. i.e. if a depends on b through workspace:* and only b has changes. Then both a and b would be published, but there'd be only changelog content for b. We'd need to manually add a "bump changelog line" for a as well, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. I was doing it the other way around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, looks good, but I think it's still incomplete. It looks like you only go one level deep. I believe this needs to work recursively, i.e. in my previous example, what if there is a package c that depends on a? It wouldn't have a changelog line. We'd need to recursively apply this logic. (But cautiously, because technically there could be circular dependencies)


// Run release build after updating package.json files
console.log('\n🔨 Running release build...');
await $({ stdio: 'inherit' })`pnpm release:build`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, but there aren't any situation where the build could be influenced by the changes we make in package.json? Maybe that's why I did it this way? 🤔 don't remember very well

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build is not influenced by package version anyways and that's what we are updating here.

});
const restorePromises = updateResults.map(
async ({ pkg, originalPackageJson, pkgJsonDirectory }) => {
// no need to restore package.json files in build directories
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it doesn't matter if we restore it anyway, right? The overhead wouldn't really be noticable, and it can save us from having to leak pkgJsonDirectory. To reduce complexity.

Copy link
Member

@Janpot Janpot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting to look really good, I still have a concern about the changelog for dependant packages that don't have changes between the two refs. Is it warranted or am I missing something?

The other comments are nits and suggestions.

@brijeshb42
Copy link
Contributor

brijeshb42 commented Oct 31, 2025

This is what the changelog looks like (with some dummy local commits) -

📦 @mui/internal-babel-plugin-display-name@1.0.4-canary.8
   Changelog:
   - [breaking] displayname by @brijeshb42

📦 @mui/internal-code-infra@0.0.3-canary.44
   Changelog:
   - Better git log parsing with author by @brijeshb42
   - Generate changelog from local commits by @brijeshb42
   - Detect affected packages from commit using git cli by @brijeshb42
   - Update package.json of built packages directly by @brijeshb42
   - Use the changelog utils to fetch all the relevant changelogs by @brijeshb42
   - Remove git cli changelog generation by @brijeshb42
   - Add GitHub release creation for canary packages by @Copilot
   - Updated dependencies:
     - Updated `@mui/internal-babel-plugin-display-name@1.0.4-canary.8`

This'll also work for packages where only dependencies are updated but they don't have any commit history of their own.
The only hardcoded check is for [breaking] in the commit message. Rest of the stuff in [] is filtered out except this.

@brijeshb42 brijeshb42 force-pushed the copilot/add-changelog-for-canary-releases branch from 10806e9 to 6494959 Compare October 31, 2025 05:31
@brijeshb42 brijeshb42 requested a review from Janpot October 31, 2025 05:39
@brijeshb42 brijeshb42 force-pushed the copilot/add-changelog-for-canary-releases branch from 6494959 to ee13d3d Compare October 31, 2025 09:09
@brijeshb42 brijeshb42 requested a review from Janpot October 31, 2025 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: code-infra Involves the code-infra product (https://www.notion.so/mui-org/5562c14178aa42af97bc1fa5114000cd). type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[code-infra] GitHub releases for Canary publishing [code-infra] Publish a github release for each canary

4 participants