Compare commits

...

20 Commits

Author SHA1 Message Date
snomiao
842515c2be [docs] Update API changelog for v1.32.4 2025-11-10 09:30:05 +00:00
sno
e36e25ebd6 Merge branch 'main' into sno-api-changelog 2025-11-10 18:26:42 +09:00
snomiao
f844d3e95b [feat] Use source file links instead of dist .d.ts in API changelog
Update changelog link generation to prefer source file locations over
generated declaration file locations. Links now point to the actual
source code where APIs are defined (e.g., src/stores/commandStore.ts)
instead of the bundled dist/index.d.ts file.

This makes the changelog much more useful as developers can click
through to see the real implementation and context.

Falls back to .d.ts location if source mapping is unavailable.

Fixes the issue where links like:
https://github.com/.../dist/index.d.ts#L1247

Are now:
https://github.com/.../src/stores/commandStore.ts#L10

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 17:22:34 +00:00
snomiao
98ed124b02 [feat] Add source file location discovery to API snapshot tool
Enhance snapshot-api.js to discover source file locations for exported
declarations using grep search. This enables mapping from generated
.d.ts declarations back to their original source files.

For each declaration, the tool now searches the src directory for:
- export interface <Name>
- export class <Name>
- export type <Name>
- export enum <Name>
- export function <Name>
- export const <Name>

The source file path and line number are stored in the snapshot for use
in generating accurate GitHub permalinks in the changelog.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 17:22:11 +00:00
snomiao
e933b5c357 [feat] Enable declarationMap for source file mapping in API changelog
Generate declaration maps to enable mapping from generated .d.ts files
back to source files. This will allow the API changelog to link to actual
source code locations instead of generated declaration files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 17:21:30 +00:00
sno
cb32562f04 Merge branch 'main' into sno-api-changelog 2025-11-06 00:54:16 +08:00
snomiao
9c94a4818f [feat] Remove Additions section and add hyperlinks to API changelog
- Remove Additions section from changelog output (no longer needed)
- Add line number tracking in API snapshots
- Generate GitHub permalinks to referenced code in changelog
- Update workflows to pass git reference for link generation
- Breaking changes and modifications now link to source code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 10:15:37 +00:00
sno
779f539b0e Merge branch 'main' into sno-api-changelog 2025-11-05 07:51:42 +08:00
snomiao
95c815f17c [docs] Add comprehensive documentation for manual API changelog workflow
- Usage examples via UI and CLI
- Detailed explanation of workflow steps
- Common use cases and troubleshooting
- Security and monitoring information

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 23:27:33 +00:00
snomiao
30dcf8c133 [feat] Add manual dispatch workflow for API changelog generation
- Allows comparing any two versions via workflow_dispatch
- Accepts version inputs (e.g., 1.29.0 and 1.30.2)
- Validates version format and tag existence
- Generates changelog and uploads as artifact (90-day retention)
- Optional PR creation for updating docs/API-CHANGELOG.md
- Provides detailed summary in workflow output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 23:21:56 +00:00
snomiao
f1193a2f86 [fix] Exclude demo-snapshots from knip checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 12:23:14 +00:00
snomiao
75daf2e4d2 [docs] Add API changelog generation demo
Add comprehensive demo showing API changelog generation across two versions (v1.29.0 → v1.30.2):

- Mock TypeScript definitions for both versions
- Generated JSON snapshots showing structured API surface
- Generated changelog with breaking changes, additions, and modifications
- Detailed README explaining the system and benefits

The demo showcases:
 Detection of breaking changes (queuePrompt → queuePromptAsync)
 Tracking of new additions (ExtensionMetadata interface, WorkflowId type)
 Identification of modifications (NodeDef enhancements, NodeStatus enum extensions)

This demonstrates the automated system that will run on every release to document API changes for extension developers.

Also exclude demo-snapshots directory from ESLint checking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 12:21:37 +00:00
sno
11922709a9 Merge branch 'main' into sno-api-changelog 2025-10-30 16:57:22 +09:00
snomiao
3742a76cfb [fix] Use current branch as PR base when triggered by push event
- Set PR base to current branch (github.ref_name) when triggered by push
- Keeps main as base for workflow_run triggers
- Allows testing on feature branches without conflicts with main

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 11:05:37 +00:00
snomiao
c84144581d [fix] Remove copied scripts before git checkout to avoid conflicts
- Clean up scripts/snapshot-api.js and scripts/compare-api-snapshots.js before checkout
- Fixes untracked working tree files conflict when returning to current version

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 11:00:37 +00:00
snomiao
8b88f8ccae [fix] Preserve API changelog scripts when checking out previous version
- Copy snapshot and comparison scripts to /tmp before checking out previous version
- Restore scripts after checkout to ensure they're available in older versions
- Fixes MODULE_NOT_FOUND error when running snapshot on previous versions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 10:55:52 +00:00
snomiao
0aab7cba4b [feat] Configure API changelog workflow to run on push to sno-api-changelog branch
- Add push trigger for sno-api-changelog branch to allow testing
- Update job condition to handle both workflow_run and push events
- Limit version comparison to two most recent versions for efficiency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 10:50:12 +00:00
sno
608874a312 Merge branch 'main' into sno-api-changelog 2025-10-29 19:44:28 +09:00
GitHub Action
5cf6ac07ac [automated] Apply ESLint and Prettier fixes 2025-10-23 03:06:37 +00:00
snomiao
ff60bdf1bc [feat] Add automated API changelog generation workflow
Implements a GitHub Actions workflow that automatically generates API
changelogs by comparing TypeScript type definitions between versions.

Changes:
- Add release-api-changelogs.yaml workflow triggered after npm types release
- Create snapshot-api.js script to extract API surface from TypeScript defs
- Create compare-api-snapshots.js to generate human-readable changelogs
- Initialize docs/API-CHANGELOG.md to track public API changes

The workflow:
1. Triggers after Release NPM Types workflow completes
2. Builds and snapshots current and previous API surfaces
3. Compares snapshots to detect additions, removals, and modifications
4. Generates formatted changelog with breaking changes highlighted
5. Creates draft PR for review before merging

This automates documentation of breaking changes for extension developers
without manual effort, supporting the large extension ecosystem.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 03:03:11 +00:00
15 changed files with 2385 additions and 2 deletions

View File

@@ -0,0 +1,234 @@
# Manual API Changelog Generation
This workflow allows you to generate API changelogs by comparing any two versions of the ComfyUI Frontend package.
## Usage
### Via GitHub Actions UI
1. Go to **Actions** tab in the repository
2. Select **Manual API Changelog Generation** from the workflows list
3. Click **Run workflow** button
4. Fill in the inputs:
- **Previous version**: The earlier version (e.g., `1.29.0` or `v1.29.0`)
- **Current version**: The later version (e.g., `1.30.2` or `v1.30.2`)
- **Create PR**: Check this to automatically create a pull request with the changelog
### Via GitHub CLI
```bash
# Basic usage - just generate changelog
gh workflow run manual-api-changelog.yaml \
-f from_version=1.29.0 \
-f to_version=1.30.2 \
-f create_pr=false
# Generate changelog and create PR
gh workflow run manual-api-changelog.yaml \
-f from_version=1.29.0 \
-f to_version=1.30.2 \
-f create_pr=true
```
## What It Does
1. **Validates Inputs**: Checks that version formats are valid (X.Y.Z) and tags exist
2. **Builds Both Versions**: Checks out each version tag, installs dependencies, and builds TypeScript types
3. **Generates Snapshots**: Creates structured JSON snapshots of the public API surface for each version
4. **Compares APIs**: Analyzes differences and categorizes as:
- ⚠️ **Breaking changes** (removals, signature changes)
-**Additions** (new interfaces, methods, properties)
- 🔄 **Modifications** (non-breaking changes)
5. **Uploads Artifact**: Saves the changelog and snapshots as a workflow artifact (90-day retention)
6. **Creates PR** (optional): Generates a draft PR to update `docs/API-CHANGELOG.md`
## Output
### Workflow Artifacts
Every run produces an artifact containing:
- `CHANGELOG-{from}-to-{to}.md` - Human-readable changelog
- `from.json` - API snapshot of the earlier version
- `to.json` - API snapshot of the later version
**Retention**: 90 days
### Pull Request (Optional)
If `create_pr` is enabled and changes are detected:
- Creates a draft PR with title: `[docs] API Changelog: v{from} → v{to}`
- Updates `docs/API-CHANGELOG.md` with the new changelog entry
- Includes detailed metadata and review instructions
- Labeled with `documentation`
## Example Changelog Output
```markdown
## v1.30.2 (2025-11-04)
Comparing v1.29.0 → v1.30.2. This changelog documents changes to the public API surface.
### ✨ Additions
**Type Aliases**
- `WorkflowId`
**Interfaces**
- `ExtensionMetadata`
- Members: `id`, `name`, `version`, `description`
### 🔄 Modifications
> **Note**: Some modifications may be breaking changes.
**Interfaces**
- `ComfyApi`
- ✨ Added member: `queuePromptAsync`
- ✨ Added member: `cancelPrompt`
- ⚠️ **Breaking**: Removed member: `queuePrompt`
**Enums**
- `NodeStatus`
- ✨ Added enum value: `ERROR`
- ✨ Added enum value: `COMPLETED`
```
## Use Cases
### 1. Generate Changelog for Missed Releases
If the automatic workflow failed or was skipped for a release:
```bash
gh workflow run manual-api-changelog.yaml \
-f from_version=1.28.0 \
-f to_version=1.29.0 \
-f create_pr=true
```
### 2. Compare Non-Adjacent Versions
To see cumulative changes across multiple releases:
```bash
gh workflow run manual-api-changelog.yaml \
-f from_version=1.25.0 \
-f to_version=1.30.2 \
-f create_pr=false
```
### 3. Test Upcoming Changes
Compare current `main` branch against the latest release (requires creating a temporary tag):
```bash
# Create temporary tag for current main
git tag v1.31.0-preview
git push origin v1.31.0-preview
# Run comparison
gh workflow run manual-api-changelog.yaml \
-f from_version=1.30.2 \
-f to_version=1.31.0-preview \
-f create_pr=false
# Clean up temporary tag
git tag -d v1.31.0-preview
git push origin :refs/tags/v1.31.0-preview
```
### 4. Audit Historical Changes
Generate changelogs for documentation purposes:
```bash
# Compare multiple version pairs
for from in 1.26.0 1.27.0 1.28.0 1.29.0; do
to=$(echo "$from" | awk -F. '{print $1"."$2+1".0"}')
gh workflow run manual-api-changelog.yaml \
-f from_version=$from \
-f to_version=$to \
-f create_pr=false
done
```
## Validation
The workflow validates:
- ✅ Version format matches semantic versioning (X.Y.Z)
- ✅ Both version tags exist in the repository
- ✅ Tags reference valid commits with buildable code
If validation fails, the workflow exits early with a clear error message.
## Limitations
- **Tag requirement**: Both versions must have corresponding `vX.Y.Z` git tags
- **Build requirement**: Both versions must have functional build processes
- **Type files**: Requires `dist/index.d.ts` to exist after building
- **Scripts**: Requires `scripts/snapshot-api.js` and `scripts/compare-api-snapshots.js` to be present
## Related Workflows
- **[Release API Changelogs](.github/workflows/release-api-changelogs.yaml)**: Automatic changelog generation triggered by NPM releases
- **[Release NPM Types](.github/workflows/release-npm-types.yaml)**: Publishes type definitions and triggers automatic changelog
## Troubleshooting
### "Tag does not exist" error
Ensure the version exists as a git tag:
```bash
git tag -l 'v*' | grep 1.29.0
```
If missing, the version may not have been released yet.
### "Build failed" error
Check that the version can be built successfully:
```bash
git checkout v1.29.0
pnpm install
pnpm build:types
```
### No changes detected
If the workflow reports no changes but you expect some:
1. Check the artifact snapshots to verify they're different
2. Ensure you're comparing the correct versions
3. Review the comparison script logic in `scripts/compare-api-snapshots.js`
### PR not created
PR creation requires:
- `create_pr` input set to `true`
- Significant changes detected (more than just headers)
- `PR_GH_TOKEN` secret configured with appropriate permissions
## Security
- **Permissions**: Workflow requires `contents: write` and `pull-requests: write`
- **Token**: Uses `secrets.PR_GH_TOKEN` for PR creation
- **Isolation**: Each workflow run uses a unique concurrency group
- **Artifacts**: Retained for 90 days, accessible to repository collaborators
## Monitoring
View workflow runs:
```bash
gh run list --workflow=manual-api-changelog.yaml
```
View specific run details:
```bash
gh run view <run-id>
```
Download artifacts:
```bash
gh run download <run-id>
```

View File

@@ -0,0 +1,255 @@
name: Manual API Changelog Generation
on:
workflow_dispatch:
inputs:
from_version:
description: 'Previous version (e.g., 1.29.0 or v1.29.0)'
required: true
type: string
to_version:
description: 'Current version (e.g., 1.30.2 or v1.30.2)'
required: true
type: string
create_pr:
description: 'Create a pull request with the changelog'
required: false
type: boolean
default: false
concurrency:
group: manual-api-changelog-${{ github.run_id }}
cancel-in-progress: false
jobs:
generate_changelog:
name: Generate API Changelog
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0 # Fetch all history for comparing versions
- name: Validate version inputs
id: validate_versions
run: |
# Normalize version strings (remove 'v' prefix if present)
FROM_VERSION="${{ github.event.inputs.from_version }}"
TO_VERSION="${{ github.event.inputs.to_version }}"
FROM_VERSION=${FROM_VERSION#v}
TO_VERSION=${TO_VERSION#v}
# Validate version format (semantic versioning)
if ! [[ "$FROM_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid from_version format: $FROM_VERSION"
echo "Expected format: X.Y.Z (e.g., 1.29.0)"
exit 1
fi
if ! [[ "$TO_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid to_version format: $TO_VERSION"
echo "Expected format: X.Y.Z (e.g., 1.30.2)"
exit 1
fi
# Check if tags exist
if ! git rev-parse "v$FROM_VERSION" >/dev/null 2>&1; then
echo "Error: Tag v$FROM_VERSION does not exist"
exit 1
fi
if ! git rev-parse "v$TO_VERSION" >/dev/null 2>&1; then
echo "Error: Tag v$TO_VERSION does not exist"
exit 1
fi
echo "from_version=$FROM_VERSION" >> $GITHUB_OUTPUT
echo "to_version=$TO_VERSION" >> $GITHUB_OUTPUT
echo "from_tag=v$FROM_VERSION" >> $GITHUB_OUTPUT
echo "to_tag=v$TO_VERSION" >> $GITHUB_OUTPUT
echo "✅ Validated versions: v$FROM_VERSION → v$TO_VERSION"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- name: Create snapshots directory
run: mkdir -p .api-snapshots
- name: Preserve scripts
run: |
# Copy scripts to temporary location
mkdir -p /tmp/api-changelog-scripts
cp scripts/snapshot-api.js scripts/compare-api-snapshots.js /tmp/api-changelog-scripts/
- name: Build and snapshot TO version
run: |
echo "Building types for v${{ steps.validate_versions.outputs.to_version }}"
git checkout ${{ steps.validate_versions.outputs.to_tag }}
# Restore scripts
mkdir -p scripts
cp /tmp/api-changelog-scripts/*.js scripts/
pnpm install --frozen-lockfile
pnpm build:types
# Generate snapshot
node scripts/snapshot-api.js dist/index.d.ts > /tmp/api-snapshots-to.json
echo "✅ Created snapshot for v${{ steps.validate_versions.outputs.to_version }}"
- name: Build and snapshot FROM version
run: |
echo "Building types for v${{ steps.validate_versions.outputs.from_version }}"
git checkout ${{ steps.validate_versions.outputs.from_tag }}
# Restore scripts
mkdir -p scripts
cp /tmp/api-changelog-scripts/*.js scripts/
pnpm install --frozen-lockfile
pnpm build:types
# Generate snapshot
node scripts/snapshot-api.js dist/index.d.ts > /tmp/api-snapshots-from.json
echo "✅ Created snapshot for v${{ steps.validate_versions.outputs.from_version }}"
- name: Return to original branch
run: |
git checkout ${{ github.ref_name }}
# Restore scripts
mkdir -p scripts
cp /tmp/api-changelog-scripts/*.js scripts/
# Copy snapshots to working directory
cp /tmp/api-snapshots-from.json .api-snapshots/from.json
cp /tmp/api-snapshots-to.json .api-snapshots/to.json
- name: Compare API snapshots and generate changelog
id: generate_changelog
run: |
# Get git ref for TO version
GIT_REF=$(git rev-parse ${{ steps.validate_versions.outputs.to_tag }})
# Run the comparison script
CHANGELOG_OUTPUT=$(node scripts/compare-api-snapshots.js \
.api-snapshots/from.json \
.api-snapshots/to.json \
${{ steps.validate_versions.outputs.from_version }} \
${{ steps.validate_versions.outputs.to_version }} \
Comfy-Org \
ComfyUI_frontend \
"$GIT_REF")
# Save changelog to file for artifact
echo "$CHANGELOG_OUTPUT" > .api-snapshots/CHANGELOG-${{ steps.validate_versions.outputs.from_version }}-to-${{ steps.validate_versions.outputs.to_version }}.md
# Also output to step summary
echo "## 📊 Generated API Changelog" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "$CHANGELOG_OUTPUT" >> $GITHUB_STEP_SUMMARY
# Check if changelog is empty or just header
if [ $(echo "$CHANGELOG_OUTPUT" | wc -l) -lt 5 ]; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "⚠️ No significant API changes detected" >> $GITHUB_STEP_SUMMARY
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "✅ API changes detected and documented" >> $GITHUB_STEP_SUMMARY
fi
echo "✅ Changelog generated successfully"
- name: Upload changelog artifact
uses: actions/upload-artifact@v4
with:
name: api-changelog-v${{ steps.validate_versions.outputs.from_version }}-to-v${{ steps.validate_versions.outputs.to_version }}
path: |
.api-snapshots/CHANGELOG-*.md
.api-snapshots/from.json
.api-snapshots/to.json
retention-days: 90
- name: Create Pull Request
if: github.event.inputs.create_pr == 'true' && steps.generate_changelog.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[docs] Update API changelog for v${{ steps.validate_versions.outputs.from_version }} → v${{ steps.validate_versions.outputs.to_version }}'
title: '[docs] API Changelog: v${{ steps.validate_versions.outputs.from_version }} → v${{ steps.validate_versions.outputs.to_version }}'
body: |
## API Changelog Update (Manual)
This PR documents public API changes between v${{ steps.validate_versions.outputs.from_version }} and v${{ steps.validate_versions.outputs.to_version }}.
The changelog has been manually generated by comparing TypeScript type definitions between versions.
### Version Comparison
- **From:** v${{ steps.validate_versions.outputs.from_version }}
- **To:** v${{ steps.validate_versions.outputs.to_version }}
- **Requested by:** @${{ github.actor }}
- **Workflow run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
### Review Instructions
- Review the changes in `docs/API-CHANGELOG.md`
- Verify accuracy of breaking changes
- Add any additional context or migration notes if needed
- Merge when ready to publish changelog
### Artifacts
The full changelog and snapshots are available as workflow artifacts for 90 days.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
branch: api-changelog-manual-${{ steps.validate_versions.outputs.from_version }}-to-${{ steps.validate_versions.outputs.to_version }}
base: ${{ github.ref_name }}
labels: documentation
delete-branch: true
draft: true
add-paths: |
docs/API-CHANGELOG.md
- name: Summary
run: |
echo "## 🎉 Workflow Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **From version:** v${{ steps.validate_versions.outputs.from_version }}" >> $GITHUB_STEP_SUMMARY
echo "- **To version:** v${{ steps.validate_versions.outputs.to_version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Changes detected:** ${{ steps.generate_changelog.outputs.has_changes }}" >> $GITHUB_STEP_SUMMARY
echo "- **Create PR:** ${{ github.event.inputs.create_pr }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📦 Artifact" >> $GITHUB_STEP_SUMMARY
echo "The generated changelog and API snapshots have been uploaded as artifacts." >> $GITHUB_STEP_SUMMARY
echo "Retention: 90 days" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event.inputs.create_pr }}" == "true" ] && [ "${{ steps.generate_changelog.outputs.has_changes }}" == "true" ]; then
echo "### 🔀 Pull Request" >> $GITHUB_STEP_SUMMARY
echo "A draft pull request has been created with the changelog updates." >> $GITHUB_STEP_SUMMARY
elif [ "${{ github.event.inputs.create_pr }}" == "true" ]; then
echo "### No PR Created" >> $GITHUB_STEP_SUMMARY
echo "No significant changes were detected, so no PR was created." >> $GITHUB_STEP_SUMMARY
else
echo "### PR Creation Skipped" >> $GITHUB_STEP_SUMMARY
echo "Pull request creation was not requested. Enable 'Create PR' option to automatically create a PR." >> $GITHUB_STEP_SUMMARY
fi

View File

@@ -0,0 +1,203 @@
name: Release API Changelogs
on:
workflow_run:
workflows: ['Release NPM Types']
types:
- completed
push:
branches:
- sno-api-changelog
concurrency:
group: release-api-changelogs-${{ github.workflow }}
cancel-in-progress: false
jobs:
generate_changelog:
name: Generate API Changelog
runs-on: ubuntu-latest
# Only run on successful completion of the Release NPM Types workflow or on push to sno-api-changelog
if: ${{ github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' }}
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0 # Fetch all history for comparing versions
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- name: Get current version
id: current_version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Current version: $VERSION"
- name: Get previous version
id: previous_version
run: |
# Get the two most recent version tags sorted
CURRENT_VERSION="${{ steps.current_version.outputs.version }}"
TAGS=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -2)
# Find the previous version tag (skip current if it exists)
PREVIOUS_TAG=""
for tag in $TAGS; do
TAG_VERSION=${tag#v}
if [ "$TAG_VERSION" != "$CURRENT_VERSION" ]; then
PREVIOUS_TAG=$tag
break
fi
done
if [ -z "$PREVIOUS_TAG" ]; then
echo "No previous version found, this may be the first release"
echo "version=" >> $GITHUB_OUTPUT
echo "tag=" >> $GITHUB_OUTPUT
else
echo "version=${PREVIOUS_TAG#v}" >> $GITHUB_OUTPUT
echo "tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
echo "Previous version: ${PREVIOUS_TAG#v}"
fi
- name: Build current types
run: pnpm build:types
- name: Snapshot current API
id: current_snapshot
run: |
# Create snapshots directory
mkdir -p .api-snapshots
# Generate snapshot of current types
node scripts/snapshot-api.js dist/index.d.ts > .api-snapshots/current.json
echo "Current API snapshot created"
- name: Preserve scripts for previous version
if: steps.previous_version.outputs.tag != ''
run: |
# Copy scripts to temporary location to use with previous version
mkdir -p /tmp/api-changelog-scripts
cp scripts/snapshot-api.js scripts/compare-api-snapshots.js /tmp/api-changelog-scripts/
- name: Checkout previous version
if: steps.previous_version.outputs.tag != ''
run: |
# Stash current changes
git stash
# Checkout previous version
git checkout ${{ steps.previous_version.outputs.tag }}
# Restore scripts
mkdir -p scripts
cp /tmp/api-changelog-scripts/*.js scripts/
- name: Build previous types
if: steps.previous_version.outputs.tag != ''
run: |
pnpm install --frozen-lockfile
pnpm build:types
- name: Snapshot previous API
if: steps.previous_version.outputs.tag != ''
run: |
# Generate snapshot of previous types
node scripts/snapshot-api.js dist/index.d.ts > .api-snapshots/previous.json
echo "Previous API snapshot created"
- name: Return to current version
if: steps.previous_version.outputs.tag != ''
run: |
# Remove copied scripts to avoid conflicts
rm -f scripts/snapshot-api.js scripts/compare-api-snapshots.js
git checkout -
git stash pop || true
- name: Compare API snapshots and generate changelog
id: generate_changelog
run: |
# Create docs directory if it doesn't exist
mkdir -p docs
# Get current git ref (commit SHA)
GIT_REF=$(git rev-parse HEAD)
# Run the comparison script
if [ -f .api-snapshots/previous.json ]; then
node scripts/compare-api-snapshots.js \
.api-snapshots/previous.json \
.api-snapshots/current.json \
${{ steps.previous_version.outputs.version }} \
${{ steps.current_version.outputs.version }} \
Comfy-Org \
ComfyUI_frontend \
"$GIT_REF" \
>> docs/API-CHANGELOG.md
else
# First release - just document the initial API surface
echo "## v${{ steps.current_version.outputs.version }} ($(date +%Y-%m-%d))" >> docs/API-CHANGELOG.md
echo "" >> docs/API-CHANGELOG.md
echo "Initial API release." >> docs/API-CHANGELOG.md
echo "" >> docs/API-CHANGELOG.md
fi
# Check if there are any changes
if git diff --quiet docs/API-CHANGELOG.md; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No API changes detected"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "API changes detected"
fi
- name: Create Pull Request
if: steps.generate_changelog.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[docs] Update API changelog for v${{ steps.current_version.outputs.version }}'
title: '[docs] API Changelog for v${{ steps.current_version.outputs.version }}'
body: |
## API Changelog Update
This PR documents public API changes between v${{ steps.previous_version.outputs.version }} and v${{ steps.current_version.outputs.version }}.
The changelog has been automatically generated by comparing TypeScript type definitions between versions.
### Review Instructions
- Review the changes in `docs/API-CHANGELOG.md`
- Verify accuracy of breaking changes
- Add any additional context or migration notes if needed
- Merge when ready to publish changelog
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
branch: api-changelog-v${{ steps.current_version.outputs.version }}
base: ${{ github.event_name == 'push' && github.ref_name || 'main' }}
labels: documentation
delete-branch: true
draft: true
add-paths: |
docs/API-CHANGELOG.md

View File

@@ -0,0 +1,49 @@
## v1.30.2 (2025-11-01)
Comparing v1.29.0 → v1.30.2. This changelog documents changes to the public API surface that third-party extensions and custom nodes depend on.
### ✨ Additions
**Type Aliases**
- `WorkflowId`
**Interfaces**
- `ExtensionMetadata`
- Members: `id`, `name`, `version`, `description`
### 🔄 Modifications
> **Note**: Some modifications may be breaking changes.
**Interfaces**
- `ComfyApi`
- ✨ Added member: `queuePromptAsync`
- ✨ Added member: `cancelPrompt`
- ✨ Added member: `getQueueStatus`
- ⚠️ **Breaking**: Removed member: `queuePrompt`
- `NodeDef`
- ✨ Added member: `input`
- ✨ Added member: `output`
- ✨ Added member: `output_name`
- `WorkflowMetadata`
- ✨ Added member: `tags`
- ✨ Added member: `thumbnail`
**Enums**
- `NodeStatus`
- ✨ Added enum value: `ERROR`
- ✨ Added enum value: `COMPLETED`
**Classes**
- `WorkflowManager`
- ✨ Added member: `cache`
- ✨ Added method: `deleteWorkflow()`
- ✨ Added method: `searchWorkflows()`
---

188
demo-snapshots/README.md Normal file
View File

@@ -0,0 +1,188 @@
# API Changelog Generation Demo
This demo showcases the automated API changelog generation system comparing two versions of the ComfyUI Frontend public API.
## Overview
The demo compares **v1.29.0****v1.30.2** to demonstrate:
- Breaking change detection
- API additions tracking
- Non-breaking modifications
- Human-readable changelog generation
## Demo Files
### Input Files
- **`v1.29.0.d.ts`** - TypeScript definitions representing the v1.29.0 API surface
- **`v1.30.2.d.ts`** - TypeScript definitions representing the v1.30.2 API surface
### Generated Files
- **`v1.29.0.json`** - Structured API snapshot from v1.29.0
- **`v1.30.2.json`** - Structured API snapshot from v1.30.2
- **`CHANGELOG-DEMO.md`** - Generated changelog comparing the two versions
## Running the Demo
```bash
# Generate API snapshots
node scripts/snapshot-api.js demo-snapshots/v1.29.0.d.ts > demo-snapshots/v1.29.0.json
node scripts/snapshot-api.js demo-snapshots/v1.30.2.d.ts > demo-snapshots/v1.30.2.json
# Compare snapshots and generate changelog
node scripts/compare-api-snapshots.js \
demo-snapshots/v1.29.0.json \
demo-snapshots/v1.30.2.json \
1.29.0 \
1.30.2 \
> demo-snapshots/CHANGELOG-DEMO.md
```
## Key Changes Detected
### ⚠️ Breaking Changes
1. **`ComfyApi.queuePrompt()` removed**
- Replaced with `queuePromptAsync()` which includes additional options
- Extension developers need to update their code to use the new async method
### ✨ New Additions
1. **New Interface: `ExtensionMetadata`**
- Provides metadata for extensions
- Fields: `id`, `name`, `version`, `description`
2. **New Type: `WorkflowId`**
- Type alias for workflow identifiers
3. **Enhanced `ComfyApi` Interface**
- `queuePromptAsync()` - Async queue with priority support
- `cancelPrompt()` - Cancel queued prompts
- `getQueueStatus()` - Query queue state
4. **Extended `NodeDef` Interface**
- `input` - Input specification
- `output` - Output types
- `output_name` - Output names
5. **Enhanced `NodeStatus` Enum**
- Added `ERROR` state
- Added `COMPLETED` state
6. **Extended `WorkflowManager` Class**
- `cache` property for workflow caching
- `deleteWorkflow()` method
- `searchWorkflows()` method
### 🔄 Non-Breaking Modifications
1. **`WorkflowMetadata` enhancements**
- Added optional `tags` field
- Added optional `thumbnail` field
## Real-World Usage
In production, this system will:
1. **Automatic Triggering**: Run after each NPM types release
2. **Version Detection**: Automatically detect current and previous versions from git tags
3. **Build Integration**: Build actual TypeScript types from the repository
4. **PR Creation**: Generate draft pull requests with the changelog
5. **Human Review**: Allow maintainers to review and enhance before merging
## Benefits for Extension Developers
### Clear Breaking Change Visibility
Extension developers can immediately see:
- What APIs were removed
- What signatures changed
- How to migrate their code
### Migration Planning
With clear documentation of additions and changes, developers can:
- Plan updates around breaking changes
- Adopt new features when ready
- Understand version compatibility
### Historical Reference
The cumulative `docs/API-CHANGELOG.md` provides:
- Complete API evolution history
- Context for design decisions
- Migration guides for major versions
## Example Extension Migration
### Before (v1.29.0)
```typescript
// Old code using queuePrompt
const result = await api.queuePrompt(workflow);
console.log('Queued:', result.prompt_id);
```
### After (v1.30.2)
```typescript
// New code using queuePromptAsync with priority
const result = await api.queuePromptAsync(workflow, { priority: 1 });
console.log('Queued:', result.prompt_id, 'Position:', result.number);
```
## Snapshot Structure
The JSON snapshots contain structured representations of:
```json
{
"types": { /* Type aliases */ },
"interfaces": { /* Interface definitions with members */ },
"enums": { /* Enum values */ },
"functions": { /* Exported functions */ },
"classes": { /* Class definitions with methods */ },
"constants": { /* Exported constants */ }
}
```
Each entry includes:
- **Name**: Identifier
- **Kind**: Type of declaration
- **Members/Methods**: Properties and functions
- **Types**: Parameter and return types
- **Visibility**: Public/private/protected modifiers
- **Optional**: Whether parameters/properties are optional
## Comparison Algorithm
The comparison script:
1. **Categorizes changes** into breaking, additions, and modifications
2. **Detects breaking changes**:
- Removed interfaces, classes, functions
- Removed methods or properties
- Changed method signatures
- Changed return types
- Removed enum values
3. **Tracks additions**:
- New interfaces, classes, types
- New methods and properties
- New enum values
4. **Identifies modifications**:
- Type changes
- Optionality changes
- Signature changes
## Future Enhancements
Planned improvements include:
- **LLM Enhancement**: Use AI to generate better descriptions and migration guides
- **Email Notifications**: Alert developers on mailing list for major changes
- **Release Notes Integration**: Auto-include in GitHub releases
- **Deprecation Tracking**: Mark APIs as deprecated before removal
- **Example Code**: Generate migration code snippets automatically
## Conclusion
This automated system ensures:
- ✅ Zero manual effort for changelog generation
- ✅ Consistent documentation format
- ✅ Clear breaking change visibility
- ✅ Historical API evolution tracking
- ✅ Better extension developer experience

58
demo-snapshots/v1.29.0.d.ts vendored Normal file
View File

@@ -0,0 +1,58 @@
/**
* Mock TypeScript definitions representing v1.29.0 API surface
* This represents the public API as it existed in version 1.29.0
*/
export interface ComfyApi {
/**
* Get API URL for backend calls
*/
apiURL(path: string): string
/**
* Get file URL for static resources
*/
fileURL(path: string): string
/**
* Queue a prompt for execution
*/
queuePrompt(prompt: object): Promise<{ prompt_id: string }>
/**
* Interrupt current execution
*/
interrupt(): Promise<void>
}
export interface NodeDef {
name: string
category: string
display_name?: string
description?: string
python_module: string
}
export enum NodeStatus {
IDLE = 'idle',
QUEUED = 'queued',
RUNNING = 'running'
}
export interface WorkflowMetadata {
title?: string
description?: string
author?: string
version?: string
}
export class WorkflowManager {
workflows: Map<string, object>
constructor()
loadWorkflow(id: string): Promise<object>
saveWorkflow(id: string, data: object): Promise<void>
}
export type NodeId = string

192
demo-snapshots/v1.29.0.json Normal file
View File

@@ -0,0 +1,192 @@
{
"types": {
"NodeId": {
"kind": "type",
"name": "NodeId",
"text": "export type NodeId = string;",
"exported": true
}
},
"interfaces": {
"ComfyApi": {
"kind": "interface",
"name": "ComfyApi",
"members": [
{
"name": "apiURL",
"kind": "method",
"parameters": [
{
"name": "path",
"type": "string",
"optional": false
}
],
"returnType": "string"
},
{
"name": "fileURL",
"kind": "method",
"parameters": [
{
"name": "path",
"type": "string",
"optional": false
}
],
"returnType": "string"
},
{
"name": "queuePrompt",
"kind": "method",
"parameters": [
{
"name": "prompt",
"type": "object",
"optional": false
}
],
"returnType": "Promise<{ prompt_id: string }>"
},
{
"name": "interrupt",
"kind": "method",
"parameters": [],
"returnType": "Promise<void>"
}
],
"exported": true,
"heritage": []
},
"NodeDef": {
"kind": "interface",
"name": "NodeDef",
"members": [
{
"name": "name",
"type": "string",
"optional": false
},
{
"name": "category",
"type": "string",
"optional": false
},
{
"name": "display_name",
"type": "string",
"optional": true
},
{
"name": "description",
"type": "string",
"optional": true
},
{
"name": "python_module",
"type": "string",
"optional": false
}
],
"exported": true,
"heritage": []
},
"WorkflowMetadata": {
"kind": "interface",
"name": "WorkflowMetadata",
"members": [
{
"name": "title",
"type": "string",
"optional": true
},
{
"name": "description",
"type": "string",
"optional": true
},
{
"name": "author",
"type": "string",
"optional": true
},
{
"name": "version",
"type": "string",
"optional": true
}
],
"exported": true,
"heritage": []
}
},
"enums": {
"NodeStatus": {
"kind": "enum",
"name": "NodeStatus",
"members": [
{
"name": "IDLE",
"value": "\"idle\""
},
{
"name": "QUEUED",
"value": "\"queued\""
},
{
"name": "RUNNING",
"value": "\"running\""
}
],
"exported": true
}
},
"functions": {},
"classes": {
"WorkflowManager": {
"kind": "class",
"name": "WorkflowManager",
"members": [
{
"name": "workflows",
"type": "Map<string, object>",
"visibility": "public"
}
],
"methods": [
{
"name": "loadWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
}
],
"returnType": "Promise<object>",
"visibility": "public"
},
{
"name": "saveWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
},
{
"name": "data",
"type": "object",
"optional": false
}
],
"returnType": "Promise<void>",
"visibility": "public"
}
],
"exported": true,
"heritage": []
}
},
"constants": {}
}

92
demo-snapshots/v1.30.2.d.ts vendored Normal file
View File

@@ -0,0 +1,92 @@
/**
* Mock TypeScript definitions representing v1.30.2 API surface
* This represents the public API with several breaking changes and additions
*/
export interface ComfyApi {
/**
* Get API URL for backend calls
*/
apiURL(path: string): string
/**
* Get file URL for static resources
*/
fileURL(path: string): string
/**
* Queue a prompt for execution (async version)
*/
queuePromptAsync(
prompt: object,
options?: { priority?: number }
): Promise<{ prompt_id: string; number: number }>
/**
* Cancel a queued prompt
*/
cancelPrompt(prompt_id: string): Promise<void>
/**
* Interrupt current execution
*/
interrupt(): Promise<void>
/**
* Get queue status
*/
getQueueStatus(): Promise<{ queue_running: any[]; queue_pending: any[] }>
}
export interface NodeDef {
name: string
category: string
display_name?: string
description?: string
python_module: string
input: {
required?: Record<string, any>
optional?: Record<string, any>
}
output: string[]
output_name: string[]
}
export enum NodeStatus {
IDLE = 'idle',
QUEUED = 'queued',
RUNNING = 'running',
ERROR = 'error',
COMPLETED = 'completed'
}
export interface WorkflowMetadata {
title?: string
description?: string
author?: string
version?: string
tags?: string[]
thumbnail?: string
}
export interface ExtensionMetadata {
id: string
name: string
version: string
description?: string
}
export class WorkflowManager {
workflows: Map<string, object>
cache: Map<string, object>
constructor()
loadWorkflow(id: string): Promise<object>
saveWorkflow(id: string, data: object): Promise<void>
deleteWorkflow(id: string): Promise<void>
searchWorkflows(query: string): Promise<object[]>
}
export type NodeId = string
export type WorkflowId = string

311
demo-snapshots/v1.30.2.json Normal file
View File

@@ -0,0 +1,311 @@
{
"types": {
"NodeId": {
"kind": "type",
"name": "NodeId",
"text": "export type NodeId = string;",
"exported": true
},
"WorkflowId": {
"kind": "type",
"name": "WorkflowId",
"text": "export type WorkflowId = string;",
"exported": true
}
},
"interfaces": {
"ComfyApi": {
"kind": "interface",
"name": "ComfyApi",
"members": [
{
"name": "apiURL",
"kind": "method",
"parameters": [
{
"name": "path",
"type": "string",
"optional": false
}
],
"returnType": "string"
},
{
"name": "fileURL",
"kind": "method",
"parameters": [
{
"name": "path",
"type": "string",
"optional": false
}
],
"returnType": "string"
},
{
"name": "queuePromptAsync",
"kind": "method",
"parameters": [
{
"name": "prompt",
"type": "object",
"optional": false
},
{
"name": "options",
"type": "{ priority?: number }",
"optional": true
}
],
"returnType": "Promise<{ prompt_id: string; number: number }>"
},
{
"name": "cancelPrompt",
"kind": "method",
"parameters": [
{
"name": "prompt_id",
"type": "string",
"optional": false
}
],
"returnType": "Promise<void>"
},
{
"name": "interrupt",
"kind": "method",
"parameters": [],
"returnType": "Promise<void>"
},
{
"name": "getQueueStatus",
"kind": "method",
"parameters": [],
"returnType": "Promise<{ queue_running: any[]; queue_pending: any[] }>"
}
],
"exported": true,
"heritage": []
},
"NodeDef": {
"kind": "interface",
"name": "NodeDef",
"members": [
{
"name": "name",
"type": "string",
"optional": false
},
{
"name": "category",
"type": "string",
"optional": false
},
{
"name": "display_name",
"type": "string",
"optional": true
},
{
"name": "description",
"type": "string",
"optional": true
},
{
"name": "python_module",
"type": "string",
"optional": false
},
{
"name": "input",
"type": "{\n required?: Record<string, any>;\n optional?: Record<string, any>;\n }",
"optional": false
},
{
"name": "output",
"type": "string[]",
"optional": false
},
{
"name": "output_name",
"type": "string[]",
"optional": false
}
],
"exported": true,
"heritage": []
},
"WorkflowMetadata": {
"kind": "interface",
"name": "WorkflowMetadata",
"members": [
{
"name": "title",
"type": "string",
"optional": true
},
{
"name": "description",
"type": "string",
"optional": true
},
{
"name": "author",
"type": "string",
"optional": true
},
{
"name": "version",
"type": "string",
"optional": true
},
{
"name": "tags",
"type": "string[]",
"optional": true
},
{
"name": "thumbnail",
"type": "string",
"optional": true
}
],
"exported": true,
"heritage": []
},
"ExtensionMetadata": {
"kind": "interface",
"name": "ExtensionMetadata",
"members": [
{
"name": "id",
"type": "string",
"optional": false
},
{
"name": "name",
"type": "string",
"optional": false
},
{
"name": "version",
"type": "string",
"optional": false
},
{
"name": "description",
"type": "string",
"optional": true
}
],
"exported": true,
"heritage": []
}
},
"enums": {
"NodeStatus": {
"kind": "enum",
"name": "NodeStatus",
"members": [
{
"name": "IDLE",
"value": "\"idle\""
},
{
"name": "QUEUED",
"value": "\"queued\""
},
{
"name": "RUNNING",
"value": "\"running\""
},
{
"name": "ERROR",
"value": "\"error\""
},
{
"name": "COMPLETED",
"value": "\"completed\""
}
],
"exported": true
}
},
"functions": {},
"classes": {
"WorkflowManager": {
"kind": "class",
"name": "WorkflowManager",
"members": [
{
"name": "workflows",
"type": "Map<string, object>",
"visibility": "public"
},
{
"name": "cache",
"type": "Map<string, object>",
"visibility": "public"
}
],
"methods": [
{
"name": "loadWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
}
],
"returnType": "Promise<object>",
"visibility": "public"
},
{
"name": "saveWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
},
{
"name": "data",
"type": "object",
"optional": false
}
],
"returnType": "Promise<void>",
"visibility": "public"
},
{
"name": "deleteWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
}
],
"returnType": "Promise<void>",
"visibility": "public"
},
{
"name": "searchWorkflows",
"parameters": [
{
"name": "query",
"type": "string",
"optional": false
}
],
"returnType": "Promise<object[]>",
"visibility": "public"
}
],
"exported": true,
"heritage": []
}
},
"constants": {}
}

40
docs/API-CHANGELOG.md Normal file
View File

@@ -0,0 +1,40 @@
# Public API Changelog
This changelog documents changes to the ComfyUI Frontend public API surface across versions. The public API surface includes types, interfaces, and objects used by third-party extensions and custom nodes.
**Important**: This is an automatically generated changelog based on TypeScript type definitions. Breaking changes are marked with ⚠️.
## What is tracked
This changelog tracks changes to the following public API components exported from `@comfyorg/comfyui-frontend-types`:
- **Type Aliases**: Type definitions used by extensions
- **Interfaces**: Object shapes and contracts
- **Enums**: Enumerated values
- **Functions**: Public utility functions
- **Classes**: Exported classes and their public members
- **Constants**: Public constant values
## Migration Guide
When breaking changes occur, refer to the specific version section below for:
- What changed
- Why it changed (if applicable)
- How to migrate your code
---
<!-- Automated changelog entries will be added below -->
## v1.32.4 (2025-11-10)
Comparing v1.32.3 → v1.32.4. This changelog documents changes to the public API surface that third-party extensions and custom nodes depend on.
### 🔄 Modifications
**Interfaces**
- [`ComfyExtension`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/e36e25ebd614c1c996e66b5c382b6b1b1bd4587a/src/types/comfy.ts#L98)
- ✨ Added member: `actionBarButtons`
---

View File

@@ -58,7 +58,8 @@ export default defineConfig([
'src/extensions/core/*',
'src/scripts/*',
'src/types/generatedManagerTypes.ts',
'src/types/vue-shim.d.ts'
'src/types/vue-shim.d.ts',
'demo-snapshots/*'
]
},
{

View File

@@ -41,7 +41,9 @@ const config: KnipConfig = {
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts'
'src/scripts/ui/components/splitButton.ts',
// Demo snapshots for API changelog system
'demo-snapshots/**'
],
compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199

View File

@@ -0,0 +1,444 @@
#!/usr/bin/env node
/**
* Compares two API snapshots and generates a human-readable changelog
* documenting additions, removals, and modifications to the public API.
*/
import * as fs from 'fs'
const args = process.argv.slice(2)
if (args.length < 4) {
console.error(
'Usage: compare-api-snapshots.js <previous.json> <current.json> <previous-version> <current-version> [repo-owner] [repo-name] [git-ref]'
)
process.exit(1)
}
const [
previousPath,
currentPath,
previousVersion,
currentVersion,
repoOwner = 'Comfy-Org',
repoName = 'ComfyUI_frontend',
gitRef = 'main'
] = args
if (!fs.existsSync(previousPath)) {
console.error(`Previous snapshot not found: ${previousPath}`)
process.exit(1)
}
if (!fs.existsSync(currentPath)) {
console.error(`Current snapshot not found: ${currentPath}`)
process.exit(1)
}
const previousApi = JSON.parse(fs.readFileSync(previousPath, 'utf-8'))
const currentApi = JSON.parse(fs.readFileSync(currentPath, 'utf-8'))
/**
* Generate GitHub permalink to source code
* Prefers source file location over dist .d.ts location
*/
function generateGitHubLink(name, item) {
// If we have source file information, use that
if (item?.sourceFile && item?.sourceLine) {
return `[\`${name}\`](https://github.com/${repoOwner}/${repoName}/blob/${gitRef}/${item.sourceFile}#L${item.sourceLine})`
}
// Fallback to .d.ts location if available
if (item?.line) {
return `[\`${name}\`](https://github.com/${repoOwner}/${repoName}/blob/${gitRef}/dist/index.d.ts#L${item.line})`
}
// No location info available
return `\`${name}\``
}
/**
* Compare two API snapshots and generate changelog
*/
function compareApis(previous, current) {
const changes = {
breaking: [],
additions: [],
modifications: [],
deprecations: []
}
const categories = [
'types',
'interfaces',
'enums',
'functions',
'classes',
'constants'
]
for (const category of categories) {
const prevItems = previous[category] || {}
const currItems = current[category] || {}
// Find additions
for (const name in currItems) {
if (!prevItems[name]) {
changes.additions.push({
category,
name,
item: currItems[name]
})
}
}
// Find removals and modifications
for (const name in prevItems) {
if (!currItems[name]) {
changes.breaking.push({
category,
name,
type: 'removed',
item: prevItems[name]
})
} else {
// Check for modifications
const diff = compareItems(prevItems[name], currItems[name], category)
if (diff.length > 0) {
changes.modifications.push({
category,
name,
changes: diff
})
}
}
}
}
return changes
}
/**
* Compare two items and return differences
*/
function compareItems(prev, curr, category) {
const differences = []
if (category === 'interfaces' || category === 'classes') {
// Compare members
const prevMembers = new Map(prev.members?.map((m) => [m.name, m]) || [])
const currMembers = new Map(curr.members?.map((m) => [m.name, m]) || [])
// Find added members
for (const [name, member] of currMembers) {
if (!prevMembers.has(name)) {
differences.push({
type: 'member_added',
name,
member
})
}
}
// Find removed members
for (const [name, member] of prevMembers) {
if (!currMembers.has(name)) {
differences.push({
type: 'member_removed',
name,
member
})
} else {
// Check if member type changed
const prevMember = prevMembers.get(name)
const currMember = currMembers.get(name)
if (prevMember.type !== currMember.type) {
differences.push({
type: 'member_type_changed',
name,
from: prevMember.type,
to: currMember.type
})
}
if (prevMember.optional !== currMember.optional) {
differences.push({
type: 'member_optionality_changed',
name,
from: prevMember.optional ? 'optional' : 'required',
to: currMember.optional ? 'optional' : 'required'
})
}
}
}
// Compare methods (for classes and interfaces)
if (category === 'classes') {
const prevMethods = new Map(prev.methods?.map((m) => [m.name, m]) || [])
const currMethods = new Map(curr.methods?.map((m) => [m.name, m]) || [])
for (const [name, method] of currMethods) {
if (!prevMethods.has(name)) {
differences.push({
type: 'method_added',
name,
method
})
}
}
for (const [name, method] of prevMethods) {
if (!currMethods.has(name)) {
differences.push({
type: 'method_removed',
name,
method
})
} else {
const prevMethod = prevMethods.get(name)
const currMethod = currMethods.get(name)
if (prevMethod.returnType !== currMethod.returnType) {
differences.push({
type: 'method_return_type_changed',
name,
from: prevMethod.returnType,
to: currMethod.returnType
})
}
// Compare parameters
if (
JSON.stringify(prevMethod.parameters) !==
JSON.stringify(currMethod.parameters)
) {
differences.push({
type: 'method_signature_changed',
name,
from: prevMethod.parameters,
to: currMethod.parameters
})
}
}
}
}
} else if (category === 'functions') {
// Compare function signatures
if (prev.returnType !== curr.returnType) {
differences.push({
type: 'return_type_changed',
from: prev.returnType,
to: curr.returnType
})
}
if (JSON.stringify(prev.parameters) !== JSON.stringify(curr.parameters)) {
differences.push({
type: 'parameters_changed',
from: prev.parameters,
to: curr.parameters
})
}
} else if (category === 'enums') {
// Compare enum members
const prevMembers = new Set(prev.members?.map((m) => m.name) || [])
const currMembers = new Set(curr.members?.map((m) => m.name) || [])
for (const member of currMembers) {
if (!prevMembers.has(member)) {
differences.push({
type: 'enum_member_added',
name: member
})
}
}
for (const member of prevMembers) {
if (!currMembers.has(member)) {
differences.push({
type: 'enum_member_removed',
name: member
})
}
}
}
return differences
}
/**
* Format changelog as markdown
*/
function formatChangelog(changes, prevVersion, currVersion) {
const lines = []
lines.push(`## v${currVersion} (${new Date().toISOString().split('T')[0]})`)
lines.push('')
lines.push(
`Comparing v${prevVersion} → v${currVersion}. This changelog documents changes to the public API surface that third-party extensions and custom nodes depend on.`
)
lines.push('')
// Breaking changes
if (changes.breaking.length > 0) {
lines.push('### ⚠️ Breaking Changes')
lines.push('')
const grouped = groupByCategory(changes.breaking)
for (const [category, items] of Object.entries(grouped)) {
lines.push(`**${categoryToTitle(category)}**`)
lines.push('')
for (const item of items) {
const displayName = generateGitHubLink(item.name, item.item)
lines.push(`- **Removed**: ${displayName}`)
}
lines.push('')
}
}
// Additions - commented out as per feedback
// if (changes.additions.length > 0) {
// lines.push('### ✨ Additions')
// lines.push('')
//
// const grouped = groupByCategory(changes.additions)
// for (const [category, items] of Object.entries(grouped)) {
// lines.push(`**${categoryToTitle(category)}**`)
// lines.push('')
// for (const item of items) {
// lines.push(`- \`${item.name}\``)
// if (item.item.members && item.item.members.length > 0) {
// const publicMembers = item.item.members.filter(
// (m) => !m.visibility || m.visibility === 'public'
// )
// if (publicMembers.length > 0 && publicMembers.length <= 5) {
// lines.push(
// ` - Members: ${publicMembers.map((m) => `\`${m.name}\``).join(', ')}`
// )
// }
// }
// }
// lines.push('')
// }
// }
// Modifications
if (changes.modifications.length > 0) {
lines.push('### 🔄 Modifications')
lines.push('')
const hasBreakingMods = changes.modifications.some((mod) =>
mod.changes.some((c) => isBreakingChange(c))
)
if (hasBreakingMods) {
lines.push('> **Note**: Some modifications may be breaking changes.')
lines.push('')
}
const grouped = groupByCategory(changes.modifications)
for (const [category, items] of Object.entries(grouped)) {
lines.push(`**${categoryToTitle(category)}**`)
lines.push('')
for (const item of items) {
// Get the current item to access source location
const currItem =
currentApi[item.category] && currentApi[item.category][item.name]
const displayName = generateGitHubLink(item.name, currItem)
lines.push(`- ${displayName}`)
for (const change of item.changes) {
const formatted = formatChange(change)
if (formatted) {
lines.push(` ${formatted}`)
}
}
}
lines.push('')
}
}
if (changes.breaking.length === 0 && changes.modifications.length === 0) {
lines.push('_No API changes detected._')
lines.push('')
}
lines.push('---')
lines.push('')
return lines.join('\n')
}
function groupByCategory(items) {
const grouped = {}
for (const item of items) {
if (!grouped[item.category]) {
grouped[item.category] = []
}
grouped[item.category].push(item)
}
return grouped
}
function categoryToTitle(category) {
const titles = {
types: 'Type Aliases',
interfaces: 'Interfaces',
enums: 'Enums',
functions: 'Functions',
classes: 'Classes',
constants: 'Constants'
}
return titles[category] || category
}
function isBreakingChange(change) {
const breakingTypes = [
'member_removed',
'method_removed',
'member_type_changed',
'method_return_type_changed',
'method_signature_changed',
'return_type_changed',
'parameters_changed',
'enum_member_removed'
]
return breakingTypes.includes(change.type)
}
function formatChange(change) {
switch (change.type) {
case 'member_added':
return `- ✨ Added member: \`${change.name}\``
case 'member_removed':
return `- ⚠️ **Breaking**: Removed member: \`${change.name}\``
case 'member_type_changed':
return `- ⚠️ **Breaking**: Member \`${change.name}\` type changed: \`${change.from}\`\`${change.to}\``
case 'member_optionality_changed':
return `- ${change.to === 'required' ? '⚠️ **Breaking**' : '✨'}: Member \`${change.name}\` is now ${change.to}`
case 'method_added':
return `- ✨ Added method: \`${change.name}()\``
case 'method_removed':
return `- ⚠️ **Breaking**: Removed method: \`${change.name}()\``
case 'method_return_type_changed':
return `- ⚠️ **Breaking**: Method \`${change.name}()\` return type changed: \`${change.from}\`\`${change.to}\``
case 'method_signature_changed':
return `- ⚠️ **Breaking**: Method \`${change.name}()\` signature changed`
case 'return_type_changed':
return `- ⚠️ **Breaking**: Return type changed: \`${change.from}\`\`${change.to}\``
case 'parameters_changed':
return `- ⚠️ **Breaking**: Function parameters changed`
case 'enum_member_added':
return `- ✨ Added enum value: \`${change.name}\``
case 'enum_member_removed':
return `- ⚠️ **Breaking**: Removed enum value: \`${change.name}\``
default:
return null
}
}
// Main execution
const changes = compareApis(previousApi, currentApi)
const changelog = formatChangelog(changes, previousVersion, currentVersion)
console.log(changelog)

313
scripts/snapshot-api.js Normal file
View File

@@ -0,0 +1,313 @@
#!/usr/bin/env node
/**
* Generates a JSON snapshot of the public API surface from TypeScript definitions.
* This snapshot is used to track API changes between versions.
*/
import { execSync } from 'child_process'
import * as fs from 'fs'
import * as path from 'path'
import * as ts from 'typescript'
const args = process.argv.slice(2)
if (args.length === 0) {
console.error('Usage: snapshot-api.js <path-to-index.d.ts>')
process.exit(1)
}
const filePath = args[0]
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`)
process.exit(1)
}
/**
* Search for the declaration in source files
* Returns {file, line} or null if not found
*/
function findInSourceFiles(declarationName, kind, sourceRoot = 'src') {
const searchPattern = getSearchPattern(declarationName, kind)
if (!searchPattern) return null
try {
// Search for the declaration pattern in source files
const result = execSync(
`grep -rn "${searchPattern}" ${sourceRoot} --include="*.ts" --include="*.tsx" | head -1`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
).trim()
if (result) {
// Parse grep output: filepath:line:content
const match = result.match(/^([^:]+):(\d+):/)
if (match) {
return {
file: match[1],
line: parseInt(match[2], 10)
}
}
}
} catch (error) {
// grep returns non-zero exit code if no match found
}
return null
}
/**
* Generate search pattern for finding declaration in source
*/
function getSearchPattern(name, kind) {
switch (kind) {
case 'interface':
return `export interface ${name}`
case 'class':
return `export class ${name}`
case 'type':
return `export type ${name}`
case 'enum':
return `export enum ${name}`
case 'function':
return `export function ${name}`
case 'constant':
return `export const ${name}`
default:
return null
}
}
/**
* Extract API surface from TypeScript definitions
*/
function extractApiSurface(sourceFile) {
const api = {
types: {},
interfaces: {},
enums: {},
functions: {},
classes: {},
constants: {}
}
function visit(node) {
// Extract type aliases
if (ts.isTypeAliasDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const sourceLocation = findInSourceFiles(name, 'type')
api.types[name] = {
kind: 'type',
name,
text: node.getText(sourceFile),
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract interfaces
if (ts.isInterfaceDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const members = []
node.members.forEach((member) => {
if (ts.isPropertySignature(member) && member.name) {
members.push({
name: member.name.getText(sourceFile),
type: member.type ? member.type.getText(sourceFile) : 'any',
optional: !!member.questionToken
})
} else if (ts.isMethodSignature(member) && member.name) {
members.push({
name: member.name.getText(sourceFile),
kind: 'method',
parameters: member.parameters.map((p) => ({
name: p.name.getText(sourceFile),
type: p.type ? p.type.getText(sourceFile) : 'any',
optional: !!p.questionToken
})),
returnType: member.type ? member.type.getText(sourceFile) : 'void'
})
}
})
const sourceLocation = findInSourceFiles(name, 'interface')
api.interfaces[name] = {
kind: 'interface',
name,
members,
exported: hasExportModifier(node),
heritage: node.heritageClauses
? node.heritageClauses
.map((clause) =>
clause.types.map((type) => type.getText(sourceFile))
)
.flat()
: [],
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract enums
if (ts.isEnumDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const members = node.members.map((member) => ({
name: member.name.getText(sourceFile),
value: member.initializer
? member.initializer.getText(sourceFile)
: undefined
}))
const sourceLocation = findInSourceFiles(name, 'enum')
api.enums[name] = {
kind: 'enum',
name,
members,
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract functions
if (ts.isFunctionDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const sourceLocation = findInSourceFiles(name, 'function')
api.functions[name] = {
kind: 'function',
name,
parameters: node.parameters.map((p) => ({
name: p.name.getText(sourceFile),
type: p.type ? p.type.getText(sourceFile) : 'any',
optional: !!p.questionToken
})),
returnType: node.type ? node.type.getText(sourceFile) : 'any',
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract classes
if (ts.isClassDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const members = []
const methods = []
node.members.forEach((member) => {
if (ts.isPropertyDeclaration(member) && member.name) {
members.push({
name: member.name.getText(sourceFile),
type: member.type ? member.type.getText(sourceFile) : 'any',
static: hasStaticModifier(member),
visibility: getVisibility(member)
})
} else if (ts.isMethodDeclaration(member) && member.name) {
methods.push({
name: member.name.getText(sourceFile),
parameters: member.parameters.map((p) => ({
name: p.name.getText(sourceFile),
type: p.type ? p.type.getText(sourceFile) : 'any',
optional: !!p.questionToken
})),
returnType: member.type ? member.type.getText(sourceFile) : 'any',
static: hasStaticModifier(member),
visibility: getVisibility(member)
})
}
})
const sourceLocation = findInSourceFiles(name, 'class')
api.classes[name] = {
kind: 'class',
name,
members,
methods,
exported: hasExportModifier(node),
heritage: node.heritageClauses
? node.heritageClauses
.map((clause) =>
clause.types.map((type) => type.getText(sourceFile))
)
.flat()
: [],
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract variable declarations (constants)
if (ts.isVariableStatement(node)) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
node.declarationList.declarations.forEach((decl) => {
if (decl.name && ts.isIdentifier(decl.name)) {
const name = decl.name.text
const sourceLocation = findInSourceFiles(name, 'constant')
api.constants[name] = {
kind: 'constant',
name,
type: decl.type ? decl.type.getText(sourceFile) : 'unknown',
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
})
}
ts.forEachChild(node, visit)
}
function hasExportModifier(node) {
return (
node.modifiers &&
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)
)
}
function hasStaticModifier(node) {
return (
node.modifiers &&
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)
)
}
function getVisibility(node) {
if (!node.modifiers) return 'public'
if (node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword))
return 'private'
if (
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ProtectedKeyword)
)
return 'protected'
return 'public'
}
visit(sourceFile)
return api
}
// Read and parse the file
const sourceCode = fs.readFileSync(filePath, 'utf-8')
const sourceFile = ts.createSourceFile(
path.basename(filePath),
sourceCode,
ts.ScriptTarget.Latest,
true
)
const apiSurface = extractApiSurface(sourceFile)
// Output as JSON
console.log(JSON.stringify(apiSurface, null, 2))

View File

@@ -2,6 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"noEmit": false,
"paths": {
"@/*": ["./src/*"]