Repository: wp-graphql/wp-graphql (language: php)

Issue:
Automate Release Cycles (Implement Changesets or other relevant solution)
As a WPGraphQL plugin contributor, I want to create a release workflow that leverages changesets in order to assist with scheduling releases and mitigate human error.

## Acceptance Criteria

Changesets is implemented in the following repos:

## Technical Details

- Prior Art - https://github.com/wpengine/wp-graphql-content-blocks
- Changesets - https://github.com/changesets/changesets
- Original Jira ticket for adding changesets to wp-graphql-content-blocks - https://wpengine.atlassian.net/browse/MERL-746


Discussion / hints:
Related: #3003

I _strongly_ recommend leaning into Conventional Commits for docs/versioning instead of something like `Changesets` that imo [adds more friction than value](https://github.com/wp-graphql/logistics-and-tracking/issues/27#issuecomment-1947073191)
@josephfusco and I discussed this a bit further. 

We see a path forward where we can leverage changesets to reduce some manual maintainer work for releases, without increasing the burden on contributors. 

My idea is that we can leverage our existing `lint-pr` workflow that enforces "Conventional Pull Request" titles and automate changeset commits from that.

If a PR is opened with a valid Conventional PR, then either a changeset commit is added to the PR itself _or_ a Changeset commit is added at the time the PR is merged (this is likely the least impact on a contributor and maintainer's existing workflows). 

I see the workflow as something like: 

- Contributor opens PR
- Lint-PR workflow runs
- Contributor (or maintainer) fixes PR title if the title is invalid
- PR is reviewed as normal
- PR is merged
- Workflow runs upon merge automatically adding a changeset commit to the `develop` branch using the Conventional PR title to describe what changed for the changeset. 
- Releases use whatever other Changeset automation to gather all changesets, update changelog, update version numbers, update `@since @todo` tags, etc.

I think this could lead to a "best of both worlds" where maintenance burden is reduced and contributors don't have to have any burden passed to them. 🤷🏻‍♂️ 

We plan to explore some workflows in the wpgraphql-ide repo to see if we can come up with some patterns/workflows that improve maintainer efficiency but do not increase contributor complexity/effort.
> I think this could lead to a "best of both worlds" where maintenance burden is reduced and contributors don't have to have any burden passed to them. 🤷🏻‍♂️

Perhaps it help me if I understand what exactly the best from "world" of changesets is supposed to be.

From the above flow it doesn't actually seem like we need changesets either. Further, creating changesets as a separate commit means the changelogs generated are likely to be [pointless](https://github.com/wpengine/wp-graphql-content-blocks/commit/2e4ac46e9751d7c144b3e0535d14ebbe3bb1fa96) or [partial and unhelpful](https://github.com/wpengine/wp-graphql-content-blocks/commit/2e7f2e854546d94d71d2f586003d77983b91edad). (Hopefully there's a way to solve this with Changeset's config, but again why bother when Conventional Commits is already a SemVer implementation)

The workflow can be simplified sans-changesets as:

```diff
* Contributor opens PR
* Lint-PR workflow runs
* Contributor (or maintainer) fixes PR title if the title is invalid.
* PR is reviewed as normal
* PR is merged
- * Workflow runs upon merge automatically adding a changeset commit to the develop branch using the Conventional PR title to describe what changed for the changeset.
- * Releases use whatever other Changeset automation to gather all changesets, update changelog, update version numbers, update @since @todo tags, etc.
+ * Releases use Conventional Commits prefixes to update changelog, update the since/todo tags etc.
```

I'm gonna hold off on any other feedback until I see something reviewable in `graphql-ide`, the above is for your consideration while experimenting with patterns.

Ya the outcome might even be not using changesets 🤷‍♂️ 

I think we made a mistake of suggesting a solution too early. 

The problem is: 

How do we reduce some of manual labor and human mistakes involved in the current release process. 

Changesets was suggested but may or may not prove to be the correct outcome. 
related: #3003 

Fix patch:
diff --git a/.changeset/README.md b/.changeset/README.md
new file mode 100644
index 000000000..7eb18fe34
--- /dev/null
+++ b/.changeset/README.md
@@ -0,0 +1,197 @@
+# Changesets
+
+This directory contains "changesets" which help us manage versioning and changelogs.
+
+## Automated Changeset Generation
+
+Changesets are automatically generated when PRs are merged based on:
+
+1. PR Title: Must follow conventional commit standards
+
+   - `feat:` = minor version bump
+   - `fix:` = patch version bump
+   - Optional scopes can be used: `feat(graphiql):`, `fix(core):`
+   - Breaking changes can be indicated by:
+     - Adding an exclamation mark: `feat!:` or `fix!:`
+     - Note: Any valid scope can be used (e.g., `feat(graphiql):`), but breaking changes should use the `!` suffix
+
+2. PR Description Sections:
+   - Breaking Changes: Used to identify major version bumps
+   - Upgrade Instructions: Added to changelog for breaking changes
+   - Description: Used as changelog entry
+
+## How Changesets Work in WPGraphQL
+
+Changesets are automatically generated when PRs are merged to track changes and automate releases. The process works as follows:
+
+1. Contributors submit PRs following our conventional commit standards
+2. GitHub Actions automate the process:
+   - Validates PR format and content
+   - Generates and commits changesets
+   - Creates releases and deploys
+
+For detailed information about the automation process, see [GitHub Workflows](../.github/workflows/README.md).
+
+## What is a Changeset?
+
+A changeset is a file that describes changes made in a PR. It includes:
+
+- Type of change (patch/minor/major)
+- PR number and link
+- Whether it contains breaking changes
+- Description of changes
+- Upgrade notes (if any)
+- Files containing `@since next-version` that need updating
+
+## Example Changeset
+
+```md
+---
+type: minor
+pr: 123
+breaking: false
+---
+
+### feat: Add new GraphQL field to Post type
+
+[PR #123](https://github.com/wp-graphql/wp-graphql/pull/123)
+
+#### Description
+
+Adds a new GraphQL field `customField` to the Post type that exposes custom meta data.
+
+#### Upgrade Notes
+
+Users implementing the PostType interface will need to implement this new field.
+
+#### Files with @since next-version
+
+- src/Type/ObjectType/PostType.php
+```
+
+## How are Changesets Used?
+
+### Release Process
+
+> **Note**: All releases are deployed to WordPress.org, with different handling for stable vs beta releases.
+
+```mermaid
+flowchart TD
+    %% PR and Changeset Process
+    PR[PR Merged] --> GC[Generate Changeset]
+    GC --> ST[Scan @since next-version tags]
+    ST --> CPR[Create Changeset PR]
+    CPR --> |Merged to develop| DEV[develop branch]
+
+    %% Standard Release Flow
+    subgraph "Standard Release"
+        DEV --> |Merge to master| M[master branch]
+        M --> VB[Version Bump]
+        VB --> SV[Sync Versions<br/>package.json<br/>wp-graphql.php<br/>constants.php]
+        SV --> US[Update @since next-version tags]
+        US --> CL[Generate Changelogs]
+
+        %% Changelog Generation
+        CL --> MD[CHANGELOG.md<br/>Developer Format]
+        CL --> RT[readme.txt<br/>WordPress.org Format]
+
+        MD & RT --> GR[Create GitHub Release]
+        GR --> WO[Deploy to WordPress.org<br/>Update Stable Tag]
+    end
+
+    %% Beta Release Flow
+    subgraph "Beta Release"
+        B[next-major branch] --> BV[Version Bump with Beta]
+        BV --> BSV[Sync Versions<br/>Keep Stable Tag]
+        BSV --> BUS[Update @since next-version tags]
+        BUS --> BCL[Generate Changelogs]
+        BCL --> BGR[Create GitHub Pre-release]
+        BGR --> BWO[Deploy to WordPress.org<br/>Keep Stable Tag]
+    end
+```
+
+When a release is created:
+
+1. All changesets are collected
+2. Version bump is determined (patch/minor/major)
+3. Changelog entries are generated for:
+   - CHANGELOG.md: Generated by @changesets/cli/changelog
+     - Detailed developer changelog
+     - Includes PR links and commit references
+     - Used by GitHub releases
+   - readme.txt: Generated by our custom formatter
+     - WordPress.org compatible format
+     - Groups changes by type (Features/Bugfixes)
+     - Updates stable tag (standard releases only)
+     - Follows WordPress.org readme standards
+4. Version numbers are updated in:
+   - constants.php
+   - package.json
+   - wp-graphql.php
+5. `@since next-version` tags are replaced with new version
+6. Changes are committed and pushed
+7. GitHub release is created
+8. Plugin is deployed to WordPress.org:
+   - Standard releases: Update stable tag to new version
+   - Beta releases: Keep stable tag pointing to last stable
+
+## Branch Strategy
+
+### Active Development (v2.x)
+
+- `master`: Current stable release (2.x.x)
+- `develop`: Development branch for next 2.x release
+- All feature/bugfix PRs target `develop`
+
+### Maintenance Mode (v1.x)
+
+> **Note**: 1.x branch is in limited support mode with manual releases only.
+
+- Only critical bug fixes and security patches
+- Limited to latest minor.patch of v1.x series
+
+### Next Major Version (v3.x)
+
+- `next-major`: Development branch for v3.0
+- Breaking changes and major features target this branch
+- Follows beta release process (see Beta Releases Guide)
+
+## Release Workflow
+
+### Current Version (v2.x)
+
+```bash
+# Ensure you're on develop with latest changes
+git checkout develop
+git pull origin develop
+
+# Create release
+npm run changeset version
+
+# Review the changes:
+# - Version numbers in constants.php, package.json, wp-graphql.php
+# - Changelog entries in CHANGELOG.md and readme.txt
+# - Stable tag in readme.txt
+# - @since next-version tags replaced with new version
+
+# Create release
+npm run changeset publish
+
+# After release, merge to master
+git checkout master
+git merge develop
+```
+
+### Beta Releases
+
+For beta releases (e.g., v3.0.0-beta.1), see [Beta Releases Guide](../docs/beta-releases.md).
+
+## Customizations
+
+WPGraphQL uses some custom changeset behaviors:
+
+1. Automatic generation from PR metadata
+2. Multi-file version updates (constants.php, wp-graphql.php, etc)
+3. Dual changelog generation (CHANGELOG.md and readme.txt)
+4. WordPress.org deployment integration
+5. `@since next-version` tag replacement
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 000000000..beb7041d4
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,20 @@
+{
+    "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
+    "changelog": [
+        "./scripts/changelog-formatters/changelog-md.js",
+        "./scripts/changelog-formatters/readme-txt.js"
+    ],
+    "commit": false,
+    "fixed": [],
+    "linked": [],
+    "access": "restricted",
+    "baseBranch": "develop",
+    "updateInternalDependencies": "patch",
+    "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
+        "onlyUpdatePeerDependentsWhenOutOfRange": true
+    },
+    "snapshot": {
+        "useCalculatedVersion": true
+    },
+    "ignore": []
+}
\ No newline at end of file
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 5230e0ee2..e959274b0 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -11,31 +11,41 @@ Thanks for sending a pull request! Please make sure you click the link above to
 
 -->
 
-What does this implement/fix? Explain your changes.
----------------------------------------------------
+## What does this implement/fix? Explain your changes.
+
 …
 
+## Breaking Changes
+
+<!--
+Does this PR introduce breaking changes? Please describe what breaks and any upgrade path for users.
+If there are no breaking changes, delete this comment and the Breaking Changes heading
+-->
+
+## Upgrade Instructions
+
+<!--
+If you indicated breaking changes above, please provide detailed upgrade instructions for users.
+If there are no breaking changes, you can delete this comment and the Upgrade Instructions Heading.
+-->
+
+## Does this close any currently open issues?
 
-Does this close any currently open issues?
-------------------------------------------
 <!--
 ### Write "closes #{pr number}"
 ### see: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
 -->
 
+## Any relevant logs, error output, GraphiQL screenshots, etc?
 
-Any relevant logs, error output, GraphiQL screenshots, etc?
--------------------------------------
-<!-- (If it’s long, please paste to https://ghostbin.com/ and insert the link here.) -->
+<!-- (If it's long, please paste to https://ghostbin.com/ and insert the link here.) -->
 
+## Any other comments?
 
-Any other comments?
--------------------
 …
 
+## Where has this been tested?
 
-Where has this been tested?
----------------------------
 **Operating System:** …
 
 **WordPress Version:** …
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
new file mode 100644
index 000000000..045939d2a
--- /dev/null
+++ b/.github/workflows/README.md
@@ -0,0 +1,143 @@
+# GitHub Actions Workflows
+
+This directory contains GitHub Actions workflows that automate our release process. Here's how they work together:
+
+## PR to Changeset Process
+
+### 1. PR Validation (`lint-pr.yml`)
+- Validates PR titles follow conventional commit format
+- Ensures proper scoping and breaking change indicators
+- Runs on PR creation and updates
+
+### 2. Changeset Generation (`generate-changeset.yml`)
+Triggered when a PR is merged:
+1. Validates:
+   - PR title format
+   - Required PR description sections
+2. Generates a changeset file containing:
+   - Version bump type (patch/minor/major)
+   - PR reference
+   - Breaking change indicators
+   - Upgrade instructions
+3. Creates a new PR with the changeset
+
+## Release Process
+
+> **Note**: All releases are deployed to WordPress.org, with different handling for stable vs beta releases.
+
+```mermaid
+flowchart TD
+    %% PR and Changeset Process
+    PR[PR Merged] --> GC[Generate Changeset]
+    GC --> ST[Scan @since todo tags]
+    ST --> CPR[Create Changeset PR]
+    CPR --> |Merged to develop| DEV[develop branch]
+
+    %% Standard Release Flow
+    subgraph "Standard Release"
+        DEV --> |Merge to master| M[master branch]
+        M --> VB[Version Bump]
+        VB --> SV[Sync Versions<br/>package.json<br/>wp-graphql.php<br/>constants.php]
+        SV --> US[Update @since tags]
+        US --> CL[Generate Changelogs]
+
+        %% Changelog Generation
+        CL --> MD[CHANGELOG.md<br/>Developer Format]
+        CL --> RT[readme.txt<br/>WordPress.org Format]
+
+        MD & RT --> GR[Create GitHub Release]
+        GR --> WO[Deploy to WordPress.org<br/>Update Stable Tag]
+    end
+
+    %% Beta Release Flow
+    subgraph "Beta Release"
+        B[next-major branch] --> BV[Version Bump with Beta]
+        BV --> BSV[Sync Versions<br/>Keep Stable Tag]
+        BSV --> BUS[Update @since tags]
+        BUS --> BCL[Generate Changelogs]
+        BCL --> BGR[Create GitHub Pre-release]
+        BGR --> BWO[Deploy to WordPress.org<br/>Keep Stable Tag]
+    end
+```
+
+### 1. Standard Releases (`deploy-to-wordpress.yml`)
+Triggered by:
+- Push to `master`
+- Manual workflow dispatch
+- Published GitHub release
+
+> **Note**: 1.x releases are handled manually and do not use this automation.
+
+Steps:
+1. Version Validation:
+   - Checks branch is `master` or `1.x/master`
+   - Verifies version numbers match across files
+2. Version and Release:
+   - Collects all changesets
+   - Updates version numbers
+   - Generates changelogs
+   - Creates git tag
+   - Creates GitHub release
+3. WordPress.org Deployment:
+   - Builds plugin
+   - Deploys to WordPress.org
+   - Updates stable tag to new version
+   - Creates release artifacts
+
+### 2. Beta Releases (`beta-release.yml`)
+Triggered by push to `next-major` branch:
+1. Validates:
+   - Branch is `next-major`
+   - Stable tag isn't modified
+2. Creates Beta Release:
+   - Enters pre-release mode if needed
+   - Updates version with beta suffix
+   - Creates GitHub pre-release
+   - Deploys to WordPress.org (keeping existing stable tag)
+
+## Changelog Generation
+
+Two changelog formats are maintained:
+1. `CHANGELOG.md`:
+   - Detailed developer changelog
+   - Includes PR links and commit references
+   - Used for GitHub releases
+2. `readme.txt`:
+   - WordPress.org compatible format
+   - Groups changes by type
+   - Updates stable tag (standard releases only)
+   - Follows WordPress.org readme standards
+
+## Version Management
+
+Version numbers are synchronized across:
+- `package.json`
+- `wp-graphql.php`
+- `constants.php`
+- `readme.txt` (stable tag for non-beta releases)
+
+## Error Handling
+
+Each workflow includes validation steps and error handling:
+- PR format validation
+- Branch validation
+- Version number consistency
+- Beta release safeguards
+- WordPress.org deployment checks
+
+## Manual Intervention
+
+Some steps require manual review:
+1. Changeset PRs:
+   - Review generated changeset
+   - Verify version bump type
+   - Check breaking change documentation
+2. Release PRs:
+   - Review changelog entries
+   - Verify version updates
+   - Check WordPress.org readme format
+
+## Related Documentation
+- [Changesets Guide](../../.changeset/README.md)
+- [Beta Releases Guide](../../docs/beta-releases.md)
+- [Upgrade Guide](../../docs/upgrading.md)
\ No newline at end of file
diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml
new file mode 100644
index 000000000..e2b27d224
--- /dev/null
+++ b/.github/workflows/beta-release.yml
@@ -0,0 +1,78 @@
+name: Beta Release
+
+on:
+  push:
+    branches:
+      - next-major
+
+jobs:
+  beta:
+    name: Create Beta Release
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Validate Branch
+        run: |
+          if [[ "$(git rev-parse --abbrev-ref HEAD)" != "next-major" ]]; then
+            echo "Error: Beta releases can only be created from next-major branch"
+            exit 1
+          fi
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+          cache: 'npm'
+
+      - name: Install Dependencies
+        run: npm ci
+
+      - name: Validate No Stable Tag Update
+        run: |
+          if grep -q "Stable tag:" readme.txt; then
+            CURRENT_TAG=$(grep "Stable tag:" readme.txt | cut -d' ' -f3)
+            if [[ "$CURRENT_TAG" != $(git show origin/master:readme.txt | grep "Stable tag:" | cut -d' ' -f3) ]]; then
+              echo "Error: Beta releases should not update the stable tag"
+              exit 1
+            fi
+          fi
+
+      - name: Create Beta Release
+        run: |
+          # Only enter pre-release mode if not already in it
+          if ! grep -q "mode: pre" .changeset/config.json; then
+            npm run changeset pre enter beta
+          fi
+
+          npm run version
+          npm run release
+
+      - name: Verify Beta Version
+        run: |
+          if ! grep -q "beta" package.json; then
+            echo "Error: Version number does not contain beta suffix"
+            exit 1
+          fi
+
+      - name: Create GitHub Pre-release
+        id: create_release
+        uses: actions/create-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          tag_name: v${{ steps.version.outputs.version }}
+          release_name: v${{ steps.version.outputs.version }}
+          body: ${{ steps.version.outputs.changelog }}
+          draft: false
+          prerelease: true
+
+      # Deploy to WordPress.org (but don't update stable tag)
+      - name: WordPress Plugin Deploy
+        uses: 10up/action-wordpress-plugin-deploy@stable
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
+          SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
+          SLUG: wp-graphql
\ No newline at end of file
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index eb628aab9..401f06867 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -34,7 +34,7 @@ jobs:
           php-version: 8.2
           tools: composer:v2
           coverage: none
-      
+
       - name: Install dependencies
         uses: ramsey/composer-install@v3
         with:
diff --git a/.github/workflows/deploy-to-wordpress.yml b/.github/workflows/deploy-to-wordpress.yml
index 2966cf882..122d93f39 100644
--- a/.github/workflows/deploy-to-wordpress.yml
+++ b/.github/workflows/deploy-to-wordpress.yml
@@ -2,7 +2,10 @@ name: Deploy to WordPress.org
 
 on:
   release:
-    types: [ published ]
+    types: [published]
+  push:
+    branches:
+      - master
   workflow_dispatch:
     inputs:
       tag:
@@ -11,11 +14,74 @@ on:
         default: 'latest'
 
 jobs:
-  tag:
-    name: New release
+  version:
+    name: Version and Release
+    if: github.event_name == 'push' && !contains(github.event.head_commit.message, 'ci skip')
     runs-on: ubuntu-latest
 
     steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Validate Branch
+        run: |
+          BRANCH=$(git rev-parse --abbrev-ref HEAD)
+          if [[ "$BRANCH" != "master" ]]; then
+            echo "Error: Releases can only be created from master"
+            exit 1
+          fi
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+          cache: 'npm'
+
+      - name: Install Dependencies
+        run: npm ci
+
+      - name: Create Release Pull Request or Publish
+        id: changesets
+        uses: changesets/action@v1
+        with:
+          version: npm run version
+          publish: npm run release
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Create GitHub Release
+        if: steps.changesets.outputs.published == 'true'
+        uses: actions/create-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          tag_name: v${{ steps.changesets.outputs.version }}
+          release_name: v${{ steps.changesets.outputs.version }}
+          body: ${{ steps.changesets.outputs.publishedPackages }}
+          draft: false
+          prerelease: false
+
+  deploy:
+    name: Deploy to WordPress.org
+    needs: [version]
+    if: needs.version.outputs.published == 'true' || github.event_name == 'release'
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Validate Version Numbers
+        run: |
+          # Check version consistency
+          PHP_VERSION=$(grep "Version:" wp-graphql.php | cut -d' ' -f2)
+          PACKAGE_VERSION=$(node -p "require('./package.json').version")
+          CONSTANTS_VERSION=$(grep "WPGRAPHQL_VERSION" constants.php | cut -d"'" -f4)
+
+          if [ "$PHP_VERSION" != "$PACKAGE_VERSION" ] || [ "$PHP_VERSION" != "$CONSTANTS_VERSION" ]; then
+            echo "Error: Version numbers do not match across files"
+            exit 1
+          fi
+
       - name: Checkout
         uses: actions/checkout@v4
         with:
diff --git a/.github/workflows/generate-changeset.yml b/.github/workflows/generate-changeset.yml
new file mode 100644
index 000000000..cc6059d86
--- /dev/null
+++ b/.github/workflows/generate-changeset.yml
@@ -0,0 +1,88 @@
+name: "Generate Changeset"
+
+# Triggered when a PR is merged
+on:
+  pull_request:
+    types: [closed]
+
+jobs:
+  generate-changeset:
+    # Only run if PR was merged (not just closed)
+    if: github.event.pull_request.merged == true
+    name: Generate Changeset
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          ref: ${{ github.event.pull_request.base.ref }}
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: "20"
+          cache: "npm"
+
+      - name: Install Dependencies
+        run: npm ci
+
+      # Scan for @since tags
+      - name: Scan for @since Tags
+        id: since_tags
+        run: |
+          TAGS=$(node -e "
+            const { generateSinceTagsMetadata } = require('./scripts/scan-since-tags');
+            generateSinceTagsMetadata().then(result => {
+              console.log(JSON.stringify(result));
+            });
+          ")
+          echo "::set-output name=tags::$TAGS"
+
+      # Generate changeset
+      - name: Generate Changeset
+        id: generate
+        env:
+          PR_TITLE: ${{ github.event.pull_request.title }}
+          PR_BODY: ${{ github.event.pull_request.body }}
+          PR_NUMBER: ${{ github.event.pull_request.number }}
+          PR_URL: ${{ github.event.pull_request.html_url }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          # Run with error handling
+          if ! node scripts/generate-changeset.js; then
+            echo "Error generating changeset"
+            exit 1
+          fi
+
+      # Create PR with changeset
+      - name: Create Pull Request
+        uses: peter-evans/create-pull-request@v5
+        with:
+          commit-message: "chore: add changeset for PR #${{ github.event.pull_request.number }}"
+          branch: changeset-pr-${{ github.event.pull_request.number }}
+          title: "chore: add changeset for PR #${{ github.event.pull_request.number }}"
+          body: |
+            This PR was automatically created to add a changeset for PR #${{ github.event.pull_request.number }}.
+
+            The changeset will be used to:
+            1. Update the changelog
+            2. Determine version bump
+            3. Update version numbers
+            4. Update @since tags
+
+            ### Validation
+            - [ ] Changeset format is correct
+            - [ ] Version bump type is appropriate
+            - [ ] Breaking changes are properly documented
+            - [ ] Upgrade instructions are included (if needed)
+            - [ ] @since tags are identified for update
+
+            ### Files with @since Tags to Update
+            ${{ fromJson(steps.since_tags.outputs.tags).sinceFiles }}
+
+            Total tags to update: ${{ fromJson(steps.since_tags.outputs.tags).totalTags }}
+          labels: |
+            automated pr
+            changeset
diff --git a/.gitignore b/.gitignore
index 759fa3508..9632bffb1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,3 +62,7 @@ c3.php
 
 # output from e2e tests
 artifacts
+
+# Changesets
+# Note: Do NOT ignore .changeset/*.md files as they are needed for release management
+.changeset/pre.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fdfe9ea50..25163a481 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
 # Changelog
 
+## 2.1.0
+
+### New Features
+
+- [#3320](https://github.com/wp-graphql/wp-graphql/pull/3320): feat: add filter to Request::is_valid_http_content_type to allow for custom content types with POST method requests
+
+### Chores / Bugfixes
+
+- [#3314](https://github.com/wp-graphql/wp-graphql/pull/3314): fix: use version_compare to simplify incompatible dependent check
+- [#3316](https://github.com/wp-graphql/wp-graphql/pull/3316): docs: update changelog and upgrade notice
+- [#3325](https://github.com/wp-graphql/wp-graphql/pull/3325): docs: update quick-start.md
+- [#3190](https://github.com/wp-graphql/wp-graphql/pull/3190): docs: add developer docs for `AbstractConnectionResolver`
+
 ## 2.0.0
 
 ### BREAKING CHANGE UPDATE
@@ -61,7 +74,7 @@ In [#3293](https://github.com/wp-graphql/wp-graphql/pull/3293) a bug was fixed i
 ### Chores / Bugfixes
 
 - [#3250](https://github.com/wp-graphql/wp-graphql/pull/3250): fix: receiving post for Incorrect uri
-- [#3268](https://github.com/wp-graphql/wp-graphql/pull/3268): ci: trigger PR workflows on release/* branches
+- [#3268](https://github.com/wp-graphql/wp-graphql/pull/3268): ci: trigger PR workflows on release/\* branches
 - [#3267](https://github.com/wp-graphql/wp-graphql/pull/3267): chore: fix bleeding edge/deprecated PHPStan smells [first pass]
 - [#3270](https://github.com/wp-graphql/wp-graphql/pull/3270): build(deps): bump the npm_and_yarn group across 1 directory with 3 updates
 - [#3271](https://github.com/wp-graphql/wp-graphql/pull/3271): fix: default cat should not be added when other categories are added
@@ -174,12 +187,12 @@ While there are no intentional breaking changes, because this change impacts eve
 ### New Features
 
 - [#3125](https://github.com/wp-graphql/wp-graphql/pull/3125): refactor: improve query handling in AbstractConnectionResolver
-    - new: `graphql_connection_pre_get_query` filter
-    - new: `AbstractConnectionResolver::is_valid_query_class()`
-    - new: `AbstractConnectionResolver::get_query()`
-    - new: `AbstractConnectionResolver::get_query_class()`
-    - new: `AsbtractConnectionResolver::query_class()`
-    - new: `AbstractConnectionResolver::$query_class`
+  - new: `graphql_connection_pre_get_query` filter
+  - new: `AbstractConnectionResolver::is_valid_query_class()`
+  - new: `AbstractConnectionResolver::get_query()`
+  - new: `AbstractConnectionResolver::get_query_class()`
+  - new: `AsbtractConnectionResolver::query_class()`
+  - new: `AbstractConnectionResolver::$query_class`
 - [#3124](https://github.com/wp-graphql/wp-graphql/pull/3124): refactor: split `AbstractConnectionResolver::get_args()` and `::get_query_args()` into `::prepare_*()` methods
 - [#3123](https://github.com/wp-graphql/wp-graphql/pull/3123): refactor: split `AbstractConnectionResolver::get_ids()` into `::prepare_ids()`
 - [#3121](https://github.com/wp-graphql/wp-graphql/pull/3121): refactor: split `AbstractConnectionResolver::get_nodes()` and `get_edges()` into `prepare_*()` methods
@@ -188,11 +201,12 @@ While there are no intentional breaking changes, because this change impacts eve
 ### Chores / Bugfixes
 
 - [#3125](https://github.com/wp-graphql/wp-graphql/pull/3125): refactor: improve query handling in AbstractConnectionResolver
-    - Implement PHPStan Generic Type
-    - Update generic Exceptions to InvariantViolation
+  - Implement PHPStan Generic Type
+  - Update generic Exceptions to InvariantViolation
 - [#3127](https://github.com/wp-graphql/wp-graphql/pull/3127): chore: update references to the WPGraphQL Slack Community to point to the new WPGraphQL Discord community instead.
 - [#3122](https://github.com/wp-graphql/wp-graphql/pull/3122): chore: relocate `AbstractConnectionResolver::is_valid_offset()` with other abstract methods.
 -
+
 ## 1.25.0
 
 ### Upgrade Notice
@@ -204,12 +218,12 @@ This release includes a fix to a regression in the v1.24.0. Users impacted by th
 - [#3104](https://github.com/wp-graphql/wp-graphql/pull/3104): feat: add `AbsractConnectionResolver::pre_should_execute()`. Thanks @justlevine!
 
 ### Chores / Bugfixes
+
 - [#3104](https://github.com/wp-graphql/wp-graphql/pull/3104): refactor: `AbstractConnectionResolver::should_execute()` Thanks @justlevine!
 - [#3112](https://github.com/wp-graphql/wp-graphql/pull/3104): fix: fixes a regression from v1.24.0 relating to field arguments defined on Interfaces not being properly merged onto Object Types that implement the interface. Thanks @kidunot89!
 - [#3114](https://github.com/wp-graphql/wp-graphql/pull/3114): fix: node IDs not showing in the Query Analyzer / X-GraphQL-Keys when using DataLoader->load_many()
 - [#3116](https://github.com/wp-graphql/wp-graphql/pull/3116): chore: Update WPGraphQLTestCase to v3. Thanks @kidunot89!
 
-
 ## 1.24.0
 
 ### Upgrade Notice
@@ -231,7 +245,6 @@ The AbstractConnectionResolver has undergone some refactoring. Some methods usin
 - [#3085](https://github.com/wp-graphql/wp-graphql/pull/3085): refactor: add AbstractConnectionResolver::prepare_page_info()and only instantiate once. Thanks @justlevine!
 - [#3083](https://github.com/wp-graphql/wp-graphql/pull/3083): refactor: deprecate camelCase methods in AbstractConnectionResolver for snake_case equivalents. Thanks @justlevine!
 
-
 ### Chores / Bugfixes
 
 - [#3095](https://github.com/wp-graphql/wp-graphql/pull/3095): chore: lint for superfluous whitespace. Thanks @justlevine!
@@ -328,7 +341,6 @@ The AbstractConnectionResolver has undergone some refactoring. Some methods usin
 
 - ci: update tests to run against WordPress 6.4.1. Update Docker Deploy to include WP 6.4.1. Update README, plugin file's "tested up to" to reflect.
 
-
 ## 1.18.1
 
 ### Chores / Bugfixes
@@ -351,7 +363,6 @@ The AbstractConnectionResolver has undergone some refactoring. Some methods usin
 - [#2977](https://github.com/wp-graphql/wp-graphql/pull/2977): chore: sort use statements alphabetically with SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses (autofix)
 - [#2978](https://github.com/wp-graphql/wp-graphql/pull/2978]): chore: implement stricter type hinting with SlevomatCodingStandard.TypeHints [phpcs]
 
-
 ## 1.17.0
 
 ### New Features
@@ -496,7 +507,6 @@ queries you have using pagination arguments (`first`, `last`, `after`, `before`)
 - [#2799](https://github.com/wp-graphql/wp-graphql/pull/2794): fix: querying posts by slug fails when custom permalinks are set
 - [#2794](https://github.com/wp-graphql/wp-graphql/pull/2794): chore(deps): bump guzzlehttp/psr7 from 1.9.0 to 1.9.1
 
-
 ## 1.14.2
 
 ### Chores / Bugfixes
@@ -519,7 +529,7 @@ queries you have using pagination arguments (`first`, `last`, `after`, `before`)
 - [#2777](https://github.com/wp-graphql/wp-graphql/pull/2777): chore: update composer dev-deps (not PHPStan). Thanks @justlevine!
 - [#2778](https://github.com/wp-graphql/wp-graphql/pull/2778): fix: Update PHPStan and fix smells. Thanks @justlevine!
 - [#2779](https://github.com/wp-graphql/wp-graphql/pull/2779): ci: test against WordPress 6.2. Thanks @justlevine!
-- [#2781](https://github.com/wp-graphql/wp-graphql/pull/2781): chore: call _doing_it_wrong() when using deprecated PostObjectUnion and TermObjectUnion. Thanks @justlevine!
+- [#2781](https://github.com/wp-graphql/wp-graphql/pull/2781): chore: call \_doing_it_wrong() when using deprecated PostObjectUnion and TermObjectUnion. Thanks @justlevine!
 - [#2782](https://github.com/wp-graphql/wp-graphql/pull/2782): ci: fix deprecation warnings in Github workflows. Thanks @justlevine!
 - [#2786](https://github.com/wp-graphql/wp-graphql/pull/2786): fix: early return for HTTP OPTIONS requests.
 
diff --git a/README.md b/README.md
index b80ce7c27..a3a9063a7 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,9 @@
 [![Actions Status](https://github.com/wp-graphql/wp-graphql/workflows/Testing%20Integration/badge.svg)](https://github.com/wp-graphql/wp-graphql/actions?query=workflow%3A%22Testing+Integration%22)
 [![Actions Status](https://github.com/wp-graphql/wp-graphql/workflows/WordPress%20Coding%20Standards/badge.svg)](https://github.com/wp-graphql/wp-graphql/actions?query=workflow%3A%22WordPress+Coding+Standards%22)
 
-**Unlock the power of WordPress data with GraphQL**  
+**Unlock the power of WordPress data with GraphQL**
 
-WPGraphQL provides an extendable GraphQL API for any WordPress site, unlocking modern development workflows.  
+WPGraphQL provides an extendable GraphQL API for any WordPress site, unlocking modern development workflows.
 
 Whether you're a WordPress developer exploring GraphQL or a GraphQL expert diving into WordPress, WPGraphQL simplifies data interaction and empowers your development workflow.
 
@@ -58,7 +58,7 @@ WPGraphQL is powered by a passionate community of contributors, backers, and spo
 
 [![Backers on Open Collective](https://opencollective.com/wp-graphql/backers/badge.svg)](https://opencollective.com/wp-graphql#backer) [![Sponsors on Open Collective](https://opencollective.com/wp-graphql/sponsors/badge.svg)](https://opencollective.com/wp-graphql#sponsor)
 
-Want to help maintain and grow WPGraphQL? 
+Want to help maintain and grow WPGraphQL?
 
 - [Support the project on Open Collective](https://opencollective.com/wp-graphql)
 - [Join the Community on Discord](https://wpgraphql.com/discord)
diff --git a/babel.config.cjs b/babel.config.cjs
new file mode 100644
index 000000000..810987f5c
--- /dev/null
+++ b/babel.config.cjs
@@ -0,0 +1,12 @@
+module.exports = {
+  presets: [
+    [
+      '@babel/preset-env',
+      {
+        targets: {
+          node: 'current',
+        },
+      },
+    ],
+  ],
+};
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 3dcdbf61e..992b7d5cd 100644
--- a/composer.json
+++ b/composer.json
@@ -29,6 +29,7 @@
 	},
 	"require-dev": {
 		"automattic/vipwpcs": "^3.0",
+		"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
 		"codeception/module-asserts": "^1.0",
 		"codeception/module-cli": "^1.0",
 		"codeception/module-db": "^1.0",
@@ -47,7 +48,8 @@
 		"slevomat/coding-standard": "^8.9",
 		"szepeviktor/phpstan-wordpress": "~2.0.1",
 		"wp-cli/wp-cli-bundle": "^2.8",
-		"wp-graphql/wp-graphql-testcase": "^3.0"
+		"wp-graphql/wp-graphql-testcase": "^3.0",
+		"composer/semver": "^3.0"
 	},
 	"config": {
 		"platform": {
@@ -90,7 +92,7 @@
 			"php ./vendor/bin/phpcs"
 		],
 		"fix-cs": [
-			"php ./vendor/bin/phpcbf"
+			"PHPCS_VERBOSE=1 php -d error_reporting=E_ALL -d display_errors=1 ./vendor/bin/phpcbf"
 		],
 		"phpstan": [
 			"phpstan analyze --ansi --memory-limit=2G -v"
@@ -104,5 +106,10 @@
 			"cd plugin-build ; zip -r wp-graphql.zip wp-graphql",
 			"rm -rf plugin-build/wp-graphql/"
 		]
+	},
+	"autoload-dev": {
+		"psr-4": {
+			"WPGraphQL\\PHPCS\\": "phpcs/WPGraphQL/PHPCS/"
+		}
 	}
-}
+}
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
index 81c5b5fef..5dacd476a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "fd55e1221e0f71bdc0a1961fb6f56828",
+    "content-hash": "3351f2b30b78da425425e5d34536093f",
     "packages": [
         {
             "name": "appsero/client",
@@ -3898,7 +3898,7 @@
                     "type": "open_collective"
                 },
                 {
-                    "url": "https://thanks.dev/phpcompatibility",
+                    "url": "https://thanks.dev/u/gh/phpcompatibility",
                     "type": "thanks_dev"
                 }
             ],
@@ -4262,30 +4262,30 @@
         },
         {
             "name": "phpstan/phpdoc-parser",
-            "version": "1.33.0",
+            "version": "2.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpdoc-parser.git",
-                "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140"
+                "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140",
-                "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140",
+                "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
+                "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.2 || ^8.0"
+                "php": "^7.4 || ^8.0"
             },
             "require-dev": {
                 "doctrine/annotations": "^2.0",
-                "nikic/php-parser": "^4.15",
+                "nikic/php-parser": "^5.3.0",
                 "php-parallel-lint/php-parallel-lint": "^1.2",
                 "phpstan/extension-installer": "^1.0",
-                "phpstan/phpstan": "^1.5",
-                "phpstan/phpstan-phpunit": "^1.1",
-                "phpstan/phpstan-strict-rules": "^1.0",
-                "phpunit/phpunit": "^9.5",
+                "phpstan/phpstan": "^2.0",
+                "phpstan/phpstan-phpunit": "^2.0",
+                "phpstan/phpstan-strict-rules": "^2.0",
+                "phpunit/phpunit": "^9.6",
                 "symfony/process": "^5.2"
             },
             "type": "library",
@@ -4303,22 +4303,22 @@
             "description": "PHPDoc parser with support for nullable, intersection and generic types",
             "support": {
                 "issues": "https://github.com/phpstan/phpdoc-parser/issues",
-                "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0"
+                "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0"
             },
-            "time": "2024-10-13T11:25:22+00:00"
+            "time": "2025-02-19T13:28:12+00:00"
         },
         {
             "name": "phpstan/phpstan",
-            "version": "2.1.4",
+            "version": "2.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "8f99e18eb775dbaf6460c95fa0b65312da9c746a"
+                "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f99e18eb775dbaf6460c95fa0b65312da9c746a",
-                "reference": "8f99e18eb775dbaf6460c95fa0b65312da9c746a",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c",
+                "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c",
                 "shasum": ""
             },
             "require": {
@@ -4363,7 +4363,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2025-02-10T08:25:21+00:00"
+            "time": "2025-02-19T15:46:42+00:00"
         },
         {
             "name": "phpstan/phpstan-deprecation-rules",
@@ -6553,32 +6553,32 @@
         },
         {
             "name": "slevomat/coding-standard",
-            "version": "8.15.0",
+            "version": "8.16.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/slevomat/coding-standard.git",
-                "reference": "7d1d957421618a3803b593ec31ace470177d7817"
+                "reference": "7748a4282df19daf966fda1d8c60a8aec803c83a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817",
-                "reference": "7d1d957421618a3803b593ec31ace470177d7817",
+                "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7748a4282df19daf966fda1d8c60a8aec803c83a",
+                "reference": "7748a4282df19daf966fda1d8c60a8aec803c83a",
                 "shasum": ""
             },
             "require": {
                 "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0",
-                "php": "^7.2 || ^8.0",
-                "phpstan/phpdoc-parser": "^1.23.1",
-                "squizlabs/php_codesniffer": "^3.9.0"
+                "php": "^7.4 || ^8.0",
+                "phpstan/phpdoc-parser": "^2.1.0",
+                "squizlabs/php_codesniffer": "^3.11.3"
             },
             "require-dev": {
-                "phing/phing": "2.17.4",
-                "php-parallel-lint/php-parallel-lint": "1.3.2",
-                "phpstan/phpstan": "1.10.60",
-                "phpstan/phpstan-deprecation-rules": "1.1.4",
-                "phpstan/phpstan-phpunit": "1.3.16",
-                "phpstan/phpstan-strict-rules": "1.5.2",
-                "phpunit/phpunit": "8.5.21|9.6.8|10.5.11"
+                "phing/phing": "3.0.1",
+                "php-parallel-lint/php-parallel-lint": "1.4.0",
+                "phpstan/phpstan": "2.1.6",
+                "phpstan/phpstan-deprecation-rules": "2.0.1",
+                "phpstan/phpstan-phpunit": "2.0.4",
+                "phpstan/phpstan-strict-rules": "2.0.3",
+                "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.9|12.0.4"
             },
             "type": "phpcodesniffer-standard",
             "extra": {
@@ -6602,7 +6602,7 @@
             ],
             "support": {
                 "issues": "https://github.com/slevomat/coding-standard/issues",
-                "source": "https://github.com/slevomat/coding-standard/tree/8.15.0"
+                "source": "https://github.com/slevomat/coding-standard/tree/8.16.0"
             },
             "funding": [
                 {
@@ -6614,7 +6614,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-03-09T15:20:58+00:00"
+            "time": "2025-02-23T18:12:49+00:00"
         },
         {
             "name": "softcreatr/jsonpath",
diff --git a/constants.php b/constants.php
index ec4cbebce..56224f352 100644
--- a/constants.php
+++ b/constants.php
@@ -18,7 +18,7 @@ function graphql_setup_constants() {
 
 	// Plugin version.
 	if ( ! defined( 'WPGRAPHQL_VERSION' ) ) {
-		define( 'WPGRAPHQL_VERSION', '2.0.0' );
+		define( 'WPGRAPHQL_VERSION', '2.1.0' );
 	}
 
 	// Plugin Folder Path.
diff --git a/docs/beta-releases.md b/docs/beta-releases.md
new file mode 100644
index 000000000..528483538
--- /dev/null
+++ b/docs/beta-releases.md
@@ -0,0 +1,130 @@
+# Beta Releases
+
+WPGraphQL uses beta releases to test major changes and breaking features before they are released to the public.
+
+> [!NOTE]
+> Beta releases are automated via GitHub Actions. See [GitHub Workflows](../.github/workflows/README.md) and [Changesets](../.changeset/README.md) for technical details.
+
+## Branch Strategy
+
+Beta releases are developed on the `next-major` branch:
+
+- Breaking changes and major features target this branch
+- Breaking changes should use the `!` suffix in PR titles (e.g., `feat!:`)
+- Regular features and fixes continue to target `develop`
+- Beta releases are not deployed to WordPress.org
+
+## Beta Release Process
+
+> [!NOTE]
+> Beta releases are deployed to WordPress.org but maintain the stable tag from the last stable release. This allows users to explicitly install beta versions while keeping stable installations on the last stable release.
+
+```mermaid
+flowchart TD
+    %% PR and Changeset Process
+    PR[PR Merged] --> GC[Generate Changeset]
+    GC --> ST[Scan @since next-version tags]
+    ST --> CPR[Create Changeset PR]
+    CPR --> |Merged to develop| DEV[develop branch]
+
+    %% Standard Release Flow
+    subgraph "Standard Release"
+        DEV --> |Merge to master| M[master branch]
+        M --> VB[Version Bump]
+        VB --> SV[Sync Versions<br/>package.json<br/>wp-graphql.php<br/>constants.php]
+        SV --> US[Update @since next-version tags]
+        US --> CL[Generate Changelogs]
+
+        %% Changelog Generation
+        CL --> MD[CHANGELOG.md<br/>Developer Format]
+        CL --> RT[readme.txt<br/>WordPress.org Format]
+
+        MD & RT --> GR[Create GitHub Release]
+        GR --> WO[Deploy to WordPress.org<br/>Update Stable Tag]
+    end
+
+    %% Beta Release Flow
+    subgraph "Beta Release"
+        B[next-major branch] --> BV[Version Bump with Beta]
+        BV --> BSV[Sync Versions<br/>Keep Stable Tag]
+        BSV --> BUS[Update @since next-version tags]
+        BUS --> BCL[Generate Changelogs]
+        BCL --> BGR[Create GitHub Pre-release]
+        BGR --> BWO[Deploy to WordPress.org<br/>Keep Stable Tag]
+    end
+```
+
+### Automated Process
+
+The beta release process is automated via GitHub Actions:
+
+1. PRs with breaking changes target the `next-major` branch
+2. When changes are merged:
+   - Version is automatically bumped with beta suffix
+   - Changelog is updated
+   - Git tag is created
+   - GitHub pre-release is created automatically
+   - Plugin is deployed to WordPress.org
+   - Stable tag remains pointing to last stable release
+
+### Changeset Generation
+
+Breaking changes are tracked through changesets, which are automatically generated when PRs are merged. To indicate a breaking change:
+
+1. Add `!` suffix to your PR title: `feat!: breaking change`
+2. Document breaking changes in PR description
+3. Include upgrade instructions in PR description
+
+The automation will handle:
+
+- Version bumping
+- Changelog updates
+- `@since` tag updates
+
+### Manual Steps
+
+Some aspects require manual review:
+
+1. Before merging PRs:
+   - Ensure breaking changes are properly documented
+   - Verify upgrade instructions are included
+   - Check `@since todo` tags are present
+2. Before releasing:
+   - Review changelog entries
+   - Verify version numbers
+   - Test beta release
+
+## Version Numbering
+
+- Beta releases: `v3.0.0-beta.1`, `v3.0.0-beta.2`, etc.
+- Final release: `v3.0.0`
+
+## Testing Beta Releases
+
+Beta releases can be tested by:
+
+1. Installing from the GitHub release assets
+2. Using Composer with the specific beta version:
+   ```json
+   {
+     "require": {
+       "wp-graphql/wp-graphql": "3.0.0-beta.1"
+     }
+   }
+   ```
+3. Cloning the repository and checking out the beta tag
+
+## Providing Feedback
+
+Feedback on beta releases can be provided through:
+
+1. GitHub Issues
+2. The beta release PR discussion
+3. WPGraphQL Discord
+
+## Notes
+
+- Beta releases are marked as pre-releases on GitHub
+- The stable tag in readme.txt is not updated for beta releases
+- Breaking changes must include upgrade notes
+- Beta releases are not deployed to WordPress.org
diff --git a/docs/contributing.md b/docs/contributing.md
index e43ec3b4f..c06b52ac3 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -5,13 +5,52 @@ title: "Contributing"
 
 This document will be most useful for developers that want to contribute to WPGraphQL and want to run the docker container locally as well as utilize xdebug for debugging and tracing.
 
+## Requirements
+
+- Node.js >= 20 (LTS)
+- npm >= 8
+- Docker
+- Composer
+- PHP 7.4+
+
+> [!NOTE]
+> WPGraphQL uses Node.js for development tooling, including testing, changelog generation, and release automation.
+
+## Development Workflow
+
+WPGraphQL uses several automated processes to maintain consistency and quality:
+
+1. **Conventional Commits**
+
+   - PR titles must follow the format (e.g., `feat:`, `fix:`)
+   - Breaking changes use `!` suffix (e.g., `feat!:`)
+   - See [Conventional Commits](https://www.conventionalcommits.org/) for more details
+
+2. **Automated Changesets**
+
+   - Generated automatically when PRs are merged
+   - Based on PR title and description
+   - Includes breaking changes and upgrade notes
+
+3. **Version Management**
+
+   - Automatically updates version numbers
+   - Updates `@since todo` tags to the appropriate version during the release process
+   - Maintains changelog in multiple formats
+
+4. **Testing**
+   - Script tests for automation code
+   - E2E tests for GraphiQL
+   - Version management validation
+   - Release simulation
+
 In order to continue, you should follow steps to setup Docker running on your machine.
 
 ### Build the WordPress Site
 
-The `app` docker image starts a running WordPress site with the local wp-graphql directory installed and activated. Local changes to the source code is immediately reflects in the app.
+The `app` docker image starts a running WordPress site with the local wp-graphql directory installed and activated. Local changes to the source code are immediately reflected in the app.
 
-First step, clone the source for wp-graphql from github.
+First step, clone the source for wp-graphql from GitHub.
 
 ```shell
 git clone git@github.com:wp-graphql/wp-graphql.git
@@ -23,13 +62,13 @@ Build the plugin and dependencies:
 composer install
 ```
 
-Or if you don't have composer installed or prefer building it in a docker instance:
+Or if you don't have Composer installed or prefer building it in a Docker instance:
 
 ```shell
 docker run -v $PWD:/app composer --ignore-platform-reqs install
 ```
 
-Build the app and testing docker images:
+Build the app and testing Docker images:
 
 ```shell
 composer build-app
@@ -42,67 +81,118 @@ In one terminal window, start the WordPress app:
 composer run-app
 ```
 
-In your web browser, open the site, [http://localhost:8091]().  And the WP admin at [http://localhost:8091/wp-admin](). Username is `admin`. Password is `password`.
+In your web browser, open the site, [http://localhost:8091](). And the WP admin at [http://localhost:8091/wp-admin](). Username is `admin`. Password is `password`.
 
 ### Using XDebug
 
 #### Local WordPress Site With XDebug
 
-Use the environment variable USING\_XDEBUG to start the docker image and WordPress with xdebug configured to use port 9003 to communicated with your IDE.
+Use the environment variable `USING_XDEBUG` to start the Docker image and WordPress with XDebug configured to use port 9003 to communicate with your IDE.
 
 ```shell
 export USING_XDEBUG=1
 composer run-app
 ```
 
-You should see output in the terminal like the following examples that indicate xdebug is indeed enabled and running in the app:
+You should see output in the terminal like the following examples that indicate XDebug is indeed enabled and running in the app:
 
 ```shell
 app_1      | Enabling XDebug 3
-app_1      | [01-Apr-2021 04:43:53 UTC] Xdebug: [Step Debug] Could not connect to debugging client. Tried: host.docker.internal:9003 (through   xdebug.client_host/xdebug.client_port) :-(
+app_1      | [01-Apr-2021 04:43:53 UTC] Xdebug: [Step Debug] Could not connect to debugging client. Tried: host.docker.internal:9003 (through xdebug.client_host/xdebug.client_port) :-(
 ```
 
-Start your IDE, like VSCode. Enable xdebug and set breakpoints. Load pages in your browsers and you should experience the IDE pausing the page load and showing the breakpoint.
+Start your IDE, like VSCode. Enable XDebug and set breakpoints. Load pages in your browsers and you should experience the IDE pausing the page load and showing the breakpoint.
 
-#### Using XDebug with unit Tests
+#### Using XDebug with Unit Tests
 
-See the testing page on running the unit test suite.  These instructions show how to enable xdebug for those unit tests and allow debugging in an IDE.
+See the testing page on running the unit test suite. These instructions show how to enable XDebug for those unit tests and allow debugging in an IDE.
 
-Use the environment variable USING\_XDEBUG to run tests with xdebug configured to use port 9003 to communicated with your IDE.
+Use the environment variable `USING_XDEBUG` to run tests with XDebug configured to use port 9003 to communicate with your IDE.
 
 ```shell
 export USING_XDEBUG=1
 composer run-tests
 ```
 
-Use the environment variable SUITES to specify individual test files for quicker runs.
+Use the environment variable `SUITES` to specify individual test files for quicker runs.
 
 #### Configure VSCode IDE Launch File
 
 Create or add the following configuration to your `.vscode/launch.json` in the root directory. Restart VSCode. Start the debug listener before running the app or testing images.
 
-If you have WordPress core files in a directory for local development, you can add the location to the `pathMappings` for debug step through.
+If you have WordPress core files in a directory for local development, you can add the location to the `pathMappings` for debug step through.
 
 ```json
 {
-    "version": "0.2.0",
-    "configurations": [
-        {
-            "name": "Listen for Xdebug",
-            "type": "php",
-            "request": "launch",
-            "port": 9003,
-            "xdebugSettings": {
-                "max_children": 128,
-                "max_data": 1024,
-                "max_depth": 3,
-                "show_hidden": 1
-            },
-            "pathMappings": {
-                "/var/www/html/wp-content/plugins/wp-graphql": "${workspaceFolder}",
-                "/var/www/html": "${workspaceFolder}/wordpress",
-            }
-        }
-    ]
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "Listen for Xdebug",
+      "type": "php",
+      "request": "launch",
+      "port": 9003,
+      "xdebugSettings": {
+        "max_children": 128,
+        "max_data": 1024,
+        "max_depth": 3,
+        "show_hidden": 1
+      },
+      "pathMappings": {
+        "/var/www/html/wp-content/plugins/wp-graphql": "${workspaceFolder}",
+        "/var/www/html": "${workspaceFolder}/wordpress"
+      }
+    }
+  ]
 }
 ```
+
+## Changesets and Releases
+
+WPGraphQL uses [changesets](../.changeset/README.md) to manage versioning and changelogs. When contributing:
+
+1. Your PR title must follow [conventional commits](https://www.conventionalcommits.org/) format:
+
+   - `feat:` for new features (minor version bump)
+   - `fix:` for bug fixes (patch version bump)
+   - Add `!` suffix for breaking changes: `feat!:` or `fix!:`
+
+2. Include in your PR description:
+
+   - Clear explanation of changes
+   - Breaking changes (if any)
+   - Upgrade instructions (if breaking)
+
+3. Add `@since todo` tags to new functions/classes
+   - These will be automatically updated during the release process.
+
+## Testing
+
+### Script Tests
+
+```shell
+# Run tests for our automation scripts
+npm run test:changesets
+```
+
+### E2E Tests
+
+```shell
+# Run GraphiQL E2E tests
+npm run test:e2e
+```
+
+### Version Management
+
+```shell
+# Check current versions across files
+npm run check-versions
+
+# Simulate a release (dry-run)
+npm run simulate-release 2.0.0
+
+# Simulate a beta release
+npm run simulate-release 2.0.0-beta.1 --beta
+```
+
+> [!NOTE]
+> The simulation commands help test the release process without actually creating releases.
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 000000000..6499fc401
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,31 @@
+module.exports = {
+    // Use babel-jest for JS/TS files
+    transform: {
+        '^.+\\.[t|j]sx?$': ['babel-jest', {
+            configFile: './babel.config.cjs',
+            rootMode: 'upward',
+            babelrc: false,
+            babelrcRoots: false
+        }]
+    },
+
+    // Specify test environment
+    testEnvironment: 'node',
+
+    // Specify where to find test files
+    testMatch: [
+        '**/scripts/__tests__/**/*.[jt]s?(x)',
+    ],
+
+    // Specify coverage collection
+    collectCoverageFrom: [
+        'scripts/**/*.{js,jsx}',
+        '!scripts/__tests__/**',
+    ],
+
+    // Setup files
+    setupFiles: [],
+
+    // Module file extensions
+    moduleFileExtensions: ['js', 'jsx', 'json', 'node'],
+};
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 1f7ab4a77..c8cc811cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "wp-graphql",
-  "version": "1.22.0",
+  "version": "2.1.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "wp-graphql",
-      "version": "1.22.0",
+      "version": "2.1.0",
       "license": "GPL-3.0",
       "dependencies": {
         "@ant-design/icons": "5.6.1",
@@ -30,6 +30,8 @@
       "devDependencies": {
         "@babel/preset-env": "^7.26.8",
         "@babel/preset-react": "^7.26.3",
+        "@changesets/cli": "^2.26.2",
+        "@changesets/get-github-info": "^0.6.0",
         "@playwright/test": "^1.50.1",
         "@testing-library/jest-dom": "^6.6.3",
         "@testing-library/react": "^14.3.1",
@@ -43,10 +45,15 @@
         "chalk": "^5.4.1",
         "concurrently": "^9.1.2",
         "dotenv": "^16.4.7",
+        "glob": "^8.1.0",
         "husky": "^9.1.7",
         "lint-staged": "^15.4.3",
         "sort-package-json": "^2.14.0",
         "webpack-remove-empty-scripts": "^1.0.4"
+      },
+      "engines": {
+        "node": ">=20",
+        "npm": ">=8"
       }
     },
     "node_modules/@adobe/css-tools": {
@@ -1964,6 +1971,370 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@changesets/apply-release-plan": {
+      "version": "7.0.9",
+      "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.9.tgz",
+      "integrity": "sha512-xB1shQP6WhflnAN+rV8eJ7j4oBgka/K62+pHuEv6jmUtSqlx2ZvJSnCGzyNfkiQmSfVsqXoI3pbAuyVpTbsKzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/config": "^3.1.0",
+        "@changesets/get-version-range-type": "^0.4.0",
+        "@changesets/git": "^3.0.2",
+        "@changesets/should-skip-package": "^0.1.2",
+        "@changesets/types": "^6.1.0",
+        "@manypkg/get-packages": "^1.1.3",
+        "detect-indent": "^6.0.0",
+        "fs-extra": "^7.0.1",
+        "lodash.startcase": "^4.4.0",
+        "outdent": "^0.5.0",
+        "prettier": "^2.7.1",
+        "resolve-from": "^5.0.0",
+        "semver": "^7.5.3"
+      }
+    },
+    "node_modules/@changesets/apply-release-plan/node_modules/detect-indent": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
+      "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@changesets/apply-release-plan/node_modules/prettier": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+      "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin-prettier.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/@changesets/apply-release-plan/node_modules/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@changesets/assemble-release-plan": {
+      "version": "6.0.6",
+      "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.6.tgz",
+      "integrity": "sha512-Frkj8hWJ1FRZiY3kzVCKzS0N5mMwWKwmv9vpam7vt8rZjLL1JMthdh6pSDVSPumHPshTTkKZ0VtNbE0cJHZZUg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/errors": "^0.2.0",
+        "@changesets/get-dependents-graph": "^2.1.3",
+        "@changesets/should-skip-package": "^0.1.2",
+        "@changesets/types": "^6.1.0",
+        "@manypkg/get-packages": "^1.1.3",
+        "semver": "^7.5.3"
+      }
+    },
+    "node_modules/@changesets/assemble-release-plan/node_modules/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@changesets/changelog-git": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz",
+      "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/types": "^6.1.0"
+      }
+    },
+    "node_modules/@changesets/cli": {
+      "version": "2.28.0",
+      "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.28.0.tgz",
+      "integrity": "sha512-of9/8Gzc+DP/Ol9Lak++Y0RsB1oO1CRzZoGIWTYcvHNREJQNqxW5tXm3YzqsA1Gx8ecZZw82FfahtiS+HkNqIw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/apply-release-plan": "^7.0.9",
+        "@changesets/assemble-release-plan": "^6.0.6",
+        "@changesets/changelog-git": "^0.2.1",
+        "@changesets/config": "^3.1.0",
+        "@changesets/errors": "^0.2.0",
+        "@changesets/get-dependents-graph": "^2.1.3",
+        "@changesets/get-release-plan": "^4.0.7",
+        "@changesets/git": "^3.0.2",
+        "@changesets/logger": "^0.1.1",
+        "@changesets/pre": "^2.0.2",
+        "@changesets/read": "^0.6.3",
+        "@changesets/should-skip-package": "^0.1.2",
+        "@changesets/types": "^6.1.0",
+        "@changesets/write": "^0.4.0",
+        "@manypkg/get-packages": "^1.1.3",
+        "ansi-colors": "^4.1.3",
+        "ci-info": "^3.7.0",
+        "enquirer": "^2.4.1",
+        "external-editor": "^3.1.0",
+        "fs-extra": "^7.0.1",
+        "mri": "^1.2.0",
+        "p-limit": "^2.2.0",
+        "package-manager-detector": "^0.2.0",
+        "picocolors": "^1.1.0",
+        "resolve-from": "^5.0.0",
+        "semver": "^7.5.3",
+        "spawndamnit": "^3.0.1",
+        "term-size": "^2.1.0"
+      },
+      "bin": {
+        "changeset": "bin.js"
+      }
+    },
+    "node_modules/@changesets/cli/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@changesets/cli/node_modules/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@changesets/config": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.0.tgz",
+      "integrity": "sha512-UbZsPkRnv2SF8Ln72B8opmNLhsazv7/M0r6GSQSQzLY++/ZPr5dDSz3L+6G2fDZ+AN1ZjsEGDdBkpEna9eJtrA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/errors": "^0.2.0",
+        "@changesets/get-dependents-graph": "^2.1.3",
+        "@changesets/logger": "^0.1.1",
+        "@changesets/types": "^6.1.0",
+        "@manypkg/get-packages": "^1.1.3",
+        "fs-extra": "^7.0.1",
+        "micromatch": "^4.0.8"
+      }
+    },
+    "node_modules/@changesets/errors": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz",
+      "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "extendable-error": "^0.1.5"
+      }
+    },
+    "node_modules/@changesets/get-dependents-graph": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz",
+      "integrity": "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/types": "^6.1.0",
+        "@manypkg/get-packages": "^1.1.3",
+        "picocolors": "^1.1.0",
+        "semver": "^7.5.3"
+      }
+    },
+    "node_modules/@changesets/get-dependents-graph/node_modules/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@changesets/get-github-info": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.6.0.tgz",
+      "integrity": "sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dataloader": "^1.4.0",
+        "node-fetch": "^2.5.0"
+      }
+    },
+    "node_modules/@changesets/get-release-plan": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.7.tgz",
+      "integrity": "sha512-FdXJ5B4ZcIWtTu+SEIAthnSScwF+mS+e657gagYUyprVLFSkAJKrA50MqoW3iOopbwQ/UhYaTESNyF9cpg1bQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/assemble-release-plan": "^6.0.6",
+        "@changesets/config": "^3.1.0",
+        "@changesets/pre": "^2.0.2",
+        "@changesets/read": "^0.6.3",
+        "@changesets/types": "^6.1.0",
+        "@manypkg/get-packages": "^1.1.3"
+      }
+    },
+    "node_modules/@changesets/get-version-range-type": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz",
+      "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@changesets/git": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.2.tgz",
+      "integrity": "sha512-r1/Kju9Y8OxRRdvna+nxpQIsMsRQn9dhhAZt94FLDeu0Hij2hnOozW8iqnHBgvu+KdnJppCveQwK4odwfw/aWQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/errors": "^0.2.0",
+        "@manypkg/get-packages": "^1.1.3",
+        "is-subdir": "^1.1.1",
+        "micromatch": "^4.0.8",
+        "spawndamnit": "^3.0.1"
+      }
+    },
+    "node_modules/@changesets/logger": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz",
+      "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picocolors": "^1.1.0"
+      }
+    },
+    "node_modules/@changesets/parse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.1.tgz",
+      "integrity": "sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/types": "^6.1.0",
+        "js-yaml": "^3.13.1"
+      }
+    },
+    "node_modules/@changesets/pre": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz",
+      "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/errors": "^0.2.0",
+        "@changesets/types": "^6.1.0",
+        "@manypkg/get-packages": "^1.1.3",
+        "fs-extra": "^7.0.1"
+      }
+    },
+    "node_modules/@changesets/read": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.3.tgz",
+      "integrity": "sha512-9H4p/OuJ3jXEUTjaVGdQEhBdqoT2cO5Ts95JTFsQyawmKzpL8FnIeJSyhTDPW1MBRDnwZlHFEM9SpPwJDY5wIg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/git": "^3.0.2",
+        "@changesets/logger": "^0.1.1",
+        "@changesets/parse": "^0.4.1",
+        "@changesets/types": "^6.1.0",
+        "fs-extra": "^7.0.1",
+        "p-filter": "^2.1.0",
+        "picocolors": "^1.1.0"
+      }
+    },
+    "node_modules/@changesets/should-skip-package": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz",
+      "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/types": "^6.1.0",
+        "@manypkg/get-packages": "^1.1.3"
+      }
+    },
+    "node_modules/@changesets/types": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz",
+      "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@changesets/write": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz",
+      "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@changesets/types": "^6.1.0",
+        "fs-extra": "^7.0.1",
+        "human-id": "^4.1.1",
+        "prettier": "^2.7.1"
+      }
+    },
+    "node_modules/@changesets/write/node_modules/prettier": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+      "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin-prettier.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
     "node_modules/@codemirror/language": {
       "version": "0.20.2",
       "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz",
@@ -3212,6 +3583,17 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/@jest/reporters/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
     "node_modules/@jest/reporters/node_modules/chalk": {
       "version": "4.1.2",
       "dev": true,
@@ -3227,6 +3609,28 @@
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "node_modules/@jest/reporters/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": {
       "version": "6.0.3",
       "dev": true,
@@ -3242,6 +3646,19 @@
         "node": ">=10"
       }
     },
+    "node_modules/@jest/reporters/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/@jest/reporters/node_modules/semver": {
       "version": "7.6.3",
       "dev": true,
@@ -3529,6 +3946,98 @@
         "@lezer/common": "^0.16.0"
       }
     },
+    "node_modules/@manypkg/find-root": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz",
+      "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "@types/node": "^12.7.1",
+        "find-up": "^4.1.0",
+        "fs-extra": "^8.1.0"
+      }
+    },
+    "node_modules/@manypkg/find-root/node_modules/@types/node": {
+      "version": "12.20.55",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
+      "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@manypkg/find-root/node_modules/fs-extra": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+      "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^4.0.0",
+        "universalify": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=6 <7 || >=8"
+      }
+    },
+    "node_modules/@manypkg/find-root/node_modules/universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/@manypkg/get-packages": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz",
+      "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "@changesets/types": "^4.0.1",
+        "@manypkg/find-root": "^1.1.0",
+        "fs-extra": "^8.1.0",
+        "globby": "^11.0.0",
+        "read-yaml-file": "^1.1.0"
+      }
+    },
+    "node_modules/@manypkg/get-packages/node_modules/@changesets/types": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz",
+      "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@manypkg/get-packages/node_modules/fs-extra": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+      "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^4.0.0",
+        "universalify": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=6 <7 || >=8"
+      }
+    },
+    "node_modules/@manypkg/get-packages/node_modules/universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
     "node_modules/@n1ru4l/push-pull-async-iterable-iterator": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz",
@@ -7974,6 +8483,29 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/better-path-resolve": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz",
+      "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-windows": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/better-path-resolve/node_modules/is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/big.js": {
       "version": "5.2.2",
       "dev": true,
@@ -9672,6 +10204,13 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/dataloader": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz",
+      "integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
     "node_modules/date-fns": {
       "version": "3.6.0",
       "license": "MIT",
@@ -9972,6 +10511,39 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/del/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/del/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/del/node_modules/globby": {
       "version": "6.1.0",
       "dev": true,
@@ -9995,6 +10567,19 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/del/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/del/node_modules/rimraf": {
       "version": "2.7.1",
       "dev": true,
@@ -11597,6 +12182,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/extendable-error": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz",
+      "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/external-editor": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@@ -12129,6 +12721,31 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/fs-extra": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+      "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.1.2",
+        "jsonfile": "^4.0.0",
+        "universalify": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=6 <7 || >=8"
+      }
+    },
+    "node_modules/fs-extra/node_modules/universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
     "node_modules/fs-monkey": {
       "version": "1.0.6",
       "dev": true,
@@ -12136,6 +12753,8 @@
     },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
       "dev": true,
       "license": "ISC"
     },
@@ -12341,19 +12960,21 @@
       }
     },
     "node_modules/glob": {
-      "version": "7.2.3",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+      "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
       "dev": true,
       "license": "ISC",
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
         "inherits": "2",
-        "minimatch": "^3.1.1",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
+        "minimatch": "^5.0.1",
+        "once": "^1.3.0"
       },
       "engines": {
-        "node": "*"
+        "node": ">=12"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -12375,24 +12996,17 @@
       "dev": true,
       "license": "BSD-2-Clause"
     },
-    "node_modules/glob/node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
     "node_modules/glob/node_modules/minimatch": {
-      "version": "3.1.2",
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+      "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
       "dev": true,
       "license": "ISC",
       "dependencies": {
-        "brace-expansion": "^1.1.7"
+        "brace-expansion": "^2.0.1"
       },
       "engines": {
-        "node": "*"
+        "node": ">=10"
       }
     },
     "node_modules/global-modules": {
@@ -12946,6 +13560,16 @@
         "node": ">= 6"
       }
     },
+    "node_modules/human-id": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz",
+      "integrity": "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "human-id": "dist/cli.js"
+      }
+    },
     "node_modules/human-signals": {
       "version": "2.1.0",
       "dev": true,
@@ -13124,6 +13748,9 @@
     },
     "node_modules/inflight": {
       "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
       "dev": true,
       "license": "ISC",
       "dependencies": {
@@ -13662,6 +14289,19 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-subdir": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz",
+      "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "better-path-resolve": "1.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/is-symbol": {
       "version": "1.1.1",
       "dev": true,
@@ -14160,6 +14800,17 @@
         }
       }
     },
+    "node_modules/jest-config/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
     "node_modules/jest-config/node_modules/chalk": {
       "version": "4.1.2",
       "dev": true,
@@ -14169,24 +14820,59 @@
         "supports-color": "^7.1.0"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/jest-config/node_modules/chalk/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/jest-config/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
       },
       "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/jest-config/node_modules/chalk/node_modules/ansi-styles": {
-      "version": "4.3.0",
+    "node_modules/jest-config/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
       "dev": true,
-      "license": "MIT",
+      "license": "ISC",
       "dependencies": {
-        "color-convert": "^2.0.1"
+        "brace-expansion": "^1.1.7"
       },
       "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+        "node": "*"
       }
     },
     "node_modules/jest-config/node_modules/pretty-format": {
@@ -14878,6 +15564,17 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/jest-runtime/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
     "node_modules/jest-runtime/node_modules/chalk": {
       "version": "4.1.2",
       "dev": true,
@@ -14893,6 +15590,41 @@
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "node_modules/jest-runtime/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/jest-runtime/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/jest-runtime/node_modules/supports-color": {
       "version": "7.2.0",
       "dev": true,
@@ -15357,6 +16089,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/jsonfile": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+      "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+      "dev": true,
+      "license": "MIT",
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
     "node_modules/jsx-ast-utils": {
       "version": "3.3.5",
       "dev": true,
@@ -15987,6 +16729,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lodash.startcase": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz",
+      "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/lodash.truncate": {
       "version": "4.4.2",
       "dev": true,
@@ -16389,6 +17138,41 @@
         "node": "^12.20.0 || >=14"
       }
     },
+    "node_modules/markdownlint-cli/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/markdownlint-cli/node_modules/glob/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/markdownlint-cli/node_modules/ignore": {
       "version": "5.2.4",
       "dev": true,
@@ -16818,6 +17602,16 @@
       "version": "1.6.5",
       "license": "Apache-2.0 WITH LLVM-exception"
     },
+    "node_modules/mri": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+      "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/mrmime": {
       "version": "2.0.0",
       "dev": true,
@@ -17145,6 +17939,52 @@
         "node": ">=10"
       }
     },
+    "node_modules/npm-packlist/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/npm-packlist/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/npm-packlist/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/npm-run-path": {
       "version": "4.0.1",
       "dev": true,
@@ -17552,6 +18392,13 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/outdent": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz",
+      "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/own-keys": {
       "version": "1.0.1",
       "dev": true,
@@ -17576,6 +18423,19 @@
         "node": ">=8"
       }
     },
+    "node_modules/p-filter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz",
+      "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-map": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/p-limit": {
       "version": "3.1.0",
       "dev": true,
@@ -17722,6 +18582,13 @@
       "dev": true,
       "license": "BlueOak-1.0.0"
     },
+    "node_modules/package-manager-detector": {
+      "version": "0.2.9",
+      "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.9.tgz",
+      "integrity": "sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/param-case": {
       "version": "3.0.4",
       "license": "MIT",
@@ -17815,6 +18682,8 @@
     },
     "node_modules/path-is-absolute": {
       "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -19778,6 +20647,32 @@
         "node": ">=8"
       }
     },
+    "node_modules/read-yaml-file": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz",
+      "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.1.5",
+        "js-yaml": "^3.6.1",
+        "pify": "^4.0.1",
+        "strip-bom": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/read-yaml-file/node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/readable-stream": {
       "version": "2.3.8",
       "dev": true,
@@ -20125,6 +21020,52 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/rimraf/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/rimraf/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rimraf/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/robots-parser": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz",
@@ -21149,6 +22090,30 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/spawndamnit": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz",
+      "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==",
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE",
+      "dependencies": {
+        "cross-spawn": "^7.0.5",
+        "signal-exit": "^4.0.1"
+      }
+    },
+    "node_modules/spawndamnit/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/spdx-correct": {
       "version": "3.2.0",
       "dev": true,
@@ -22089,6 +23054,19 @@
         "node": ">= 6"
       }
     },
+    "node_modules/term-size": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
+      "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/terminal-link": {
       "version": "2.1.1",
       "dev": true,
@@ -22228,6 +23206,28 @@
         "concat-map": "0.0.1"
       }
     },
+    "node_modules/test-exclude/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/test-exclude/node_modules/minimatch": {
       "version": "3.1.2",
       "dev": true,
diff --git a/package.json b/package.json
index 108bec45c..234f9d6c2 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,24 @@
 {
   "name": "wp-graphql",
   "private": true,
-  "version": "2.0.0",
+  "version": "2.1.0",
   "description": "GraphQL API for WordPress",
   "homepage": "https://github.com/wp-graphql/wp-graphql#readme",
   "author": "WPGraphQL <info@wpgraphql.com> (https://www.wpgraphql.com)",
   "license": "GPL-3.0",
   "email": "info@wpgraphql.com",
   "main": "index.js",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/wp-graphql/wp-graphql.git"
+  },
+  "keywords": [
+    "WordPress",
+    "GraphQL"
+  ],
+  "bugs": {
+    "url": "https://github.com/wp-graphql/wp-graphql/issues"
+  },
   "directories": {
     "doc": "docs",
     "test": "tests"
@@ -16,6 +27,10 @@
     "check-engines": "wp-scripts check-engines",
     "build": "wp-scripts build",
     "start": "wp-scripts start",
+    "check-versions": "node scripts/check-versions.js",
+    "update-since": "node scripts/update-since-tags.js",
+    "simulate-workflow": "node scripts/simulate-workflow.js",
+    "test:changesets": "jest scripts/__tests__ --verbose --detectOpenHandles --forceExit",
     "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js",
     "test:e2e:ui": "wp-scripts test-playwright --config tests/e2e/playwright.config.js --ui",
     "test:unit": "wp-scripts test-unit-js --config tests/js-unit/jest.config.js",
@@ -25,18 +40,10 @@
     "lint:js:fix": "wp-scripts lint-js --fix ./src",
     "lint:js:src": "wp-scripts lint-js ./src",
     "wp-env": "wp-env",
-    "prepare": "husky install"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/wp-graphql/wp-graphql.git"
-  },
-  "keywords": [
-    "WordPress",
-    "GraphQL"
-  ],
-  "bugs": {
-    "url": "https://github.com/wp-graphql/wp-graphql/issues"
+    "prepare": "husky install",
+    "changeset": "changeset",
+    "version": "changeset version",
+    "release": "changeset publish"
   },
   "dependencies": {
     "@ant-design/icons": "5.6.1",
@@ -58,6 +65,8 @@
     "use-query-params": "1.2.3"
   },
   "devDependencies": {
+    "@changesets/cli": "^2.26.2",
+    "@changesets/get-github-info": "^0.6.0",
     "@babel/preset-env": "^7.26.8",
     "@babel/preset-react": "^7.26.3",
     "@playwright/test": "^1.50.1",
@@ -74,6 +83,7 @@
     "concurrently": "^9.1.2",
     "dotenv": "^16.4.7",
     "husky": "^9.1.7",
+    "glob": "^8.1.0",
     "lint-staged": "^15.4.3",
     "sort-package-json": "^2.14.0",
     "webpack-remove-empty-scripts": "^1.0.4"
@@ -84,5 +94,9 @@
   "overrides": {
     "react": "18.2.0",
     "react-dom": "18.2.0"
+  },
+  "engines": {
+    "node": ">=20",
+    "npm": ">=8"
   }
 }
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index e614a7316..2b1e59da3 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -195,4 +195,6 @@
 			<property name="skipIfInheritdoc" value="true" />
 		</properties>
 	</rule>
+	<rule ref="./phpcs/WPGraphQL/PHPCS/Sniffs/Functions/VersionParameterSniff.php"/>
+	<rule ref="./phpcs/WPGraphQL/PHPCS/Sniffs/Commenting/ValidSinceTagSniff.php"/>
 </ruleset>
diff --git a/phpcs/WPGraphQL/PHPCS/Sniffs/Commenting/ValidSinceTagSniff.php b/phpcs/WPGraphQL/PHPCS/Sniffs/Commenting/ValidSinceTagSniff.php
new file mode 100644
index 000000000..acb2faf67
--- /dev/null
+++ b/phpcs/WPGraphQL/PHPCS/Sniffs/Commenting/ValidSinceTagSniff.php
@@ -0,0 +1,149 @@
+<?php
+namespace WPGraphQL\PHPCS\Sniffs\Commenting;
+
+use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Files\File;
+use Composer\Semver\VersionParser;
+
+class ValidSinceTagSniff implements Sniff
+{
+    /**
+     * Version parser instance
+     *
+     * @var VersionParser
+     */
+    private $versionParser;
+
+    /**
+     * Valid version placeholder strings
+     *
+     * @var array<string>
+     */
+    private $validPlaceholders = [
+        'next-version',
+        'todo',
+    ];
+
+    public function __construct()
+    {
+        $this->versionParser = new VersionParser();
+    }
+
+    /**
+     * Returns an array of tokens this test wants to listen for.
+     *
+     * @return array<int|string>
+     */
+    public function register()
+    {
+        return [T_DOC_COMMENT_TAG, T_COMMENT];
+    }
+
+    /**
+     * Processes this test when one of its tokens is encountered.
+     *
+     * @param File $phpcsFile The file being scanned.
+     * @param int  $stackPtr  The position of the current token in the stack.
+     *
+     * @return void
+     */
+    public function process(File $phpcsFile, $stackPtr)
+    {
+        // Skip files in certain directories
+        $file = $phpcsFile->getFilename();
+        $skipDirs = [
+            'scripts/__tests__',
+            '.changeset',
+            'docs',
+        ];
+
+        foreach ($skipDirs as $dir) {
+            if (strpos($file, $dir) !== false) {
+                return;
+            }
+        }
+
+        $tokens = $phpcsFile->getTokens();
+
+        // Only process @since tags
+        if ($tokens[$stackPtr]['content'] !== '@since') {
+            return;
+        }
+
+        // Get the version string (next token after @since)
+        $versionPtr = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $stackPtr + 1, null, false, null, true);
+        if ($versionPtr === false) {
+            return;
+        }
+
+        $version = $tokens[$versionPtr]['content'];
+
+        // Split on first space to get just the version number
+        $versionParts = preg_split('/\s+/', $version, 2);
+        $version = $versionParts[0];
+
+        // Check if it's a valid placeholder
+        if (in_array($version, $this->validPlaceholders, true)) {
+            // If using old placeholders, suggest next-version
+            if ($version !== 'next-version') {
+                $fix = $phpcsFile->addFixableWarning(
+                    'Please use "@since next-version" instead of "@since %s"',
+                    $versionPtr,
+                    'OldVersionPlaceholder',
+                    [$version]
+                );
+
+                if ($fix === true) {
+                    $this->fixVersion($phpcsFile, $versionPtr, $version, 'next-version');
+                }
+            }
+            return;
+        }
+
+        // Validate semver
+        if (!$this->isValidSemver($version)) {
+            $fix = $phpcsFile->addFixableError(
+                'Version for @since tag must be a valid semver version or "next-version" but got "%s"',
+                $versionPtr,
+                'InvalidVersion',
+                [$version]
+            );
+
+            if ($fix === true) {
+                $this->fixVersion($phpcsFile, $versionPtr, $version, 'next-version');
+            }
+        }
+    }
+
+    private function fixVersion(File $phpcsFile, $versionPtr, $oldVersion, $newVersion)
+    {
+        $tokens = $phpcsFile->getTokens();
+        $content = $tokens[$versionPtr]['content'];
+
+        // Replace just the version part, keeping any description that follows
+        $newContent = str_replace($oldVersion, $newVersion, $content);
+
+        $phpcsFile->fixer->beginChangeset();
+        $phpcsFile->fixer->replaceToken($versionPtr, $newContent);
+        $phpcsFile->fixer->endChangeset();
+
+        return true;
+    }
+
+    /**
+     * Check if a version string is valid semver.
+     *
+     * @param string $version Version string to check.
+     *
+     * @return bool
+     */
+    private function isValidSemver($version)
+    {
+        try {
+            $this->versionParser->normalize($version);
+            return true;
+        } catch (\UnexpectedValueException $e) {
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/phpcs/WPGraphQL/PHPCS/Sniffs/Functions/VersionParameterSniff.php b/phpcs/WPGraphQL/PHPCS/Sniffs/Functions/VersionParameterSniff.php
new file mode 100644
index 000000000..3d535b77c
--- /dev/null
+++ b/phpcs/WPGraphQL/PHPCS/Sniffs/Functions/VersionParameterSniff.php
@@ -0,0 +1,246 @@
+<?php
+namespace WPGraphQL\PHPCS\Sniffs\Functions;
+
+use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Files\File;
+use Composer\Semver\VersionParser;
+
+class VersionParameterSniff implements Sniff
+{
+    /**
+     * Version parser instance
+     *
+     * @var VersionParser
+     */
+    private $versionParser;
+
+    /**
+     * Functions to check and their version parameter position (1-based)
+     *
+     * @var array<string,int>
+     */
+    private $functions = [
+        '_doing_it_wrong' => 3,
+        '_deprecated_function' => 2,
+        '_deprecated_file' => 2,
+        '_deprecated_argument' => 2,
+        '_deprecated_hook' => 2,
+        '_deprecated_class' => 2,
+        '_deprecated_constructor' => 2,
+    ];
+
+    /**
+     * Valid version placeholder strings
+     *
+     * @var array<string>
+     */
+    private $validPlaceholders = [
+        '@since next-version',
+        '@since todo',
+        '@next-version',
+    ];
+
+    public function __construct()
+    {
+        $this->versionParser = new VersionParser();
+    }
+
+    /**
+     * Returns an array of tokens this test wants to listen for.
+     *
+     * @return array<int|string>
+     */
+    public function register()
+    {
+        return [T_STRING];
+    }
+
+    /**
+     * Processes this test when one of its tokens is encountered.
+     *
+     * @param File $phpcsFile The file being scanned.
+     * @param int  $stackPtr  The position of the current token in the stack.
+     *
+     * @return void
+     */
+    public function process(File $phpcsFile, $stackPtr)
+    {
+        // Skip files in certain directories
+        $file = $phpcsFile->getFilename();
+        $skipDirs = [
+            'scripts/__tests__',
+            '.changeset',
+            'docs',
+        ];
+
+        foreach ($skipDirs as $dir) {
+            if (strpos($file, $dir) !== false) {
+                return;
+            }
+        }
+
+        $tokens = $phpcsFile->getTokens();
+        $functionName = $tokens[$stackPtr]['content'];
+
+        // Check if this is one of our target functions
+        if (!isset($this->functions[$functionName])) {
+            return;
+        }
+
+        // Make sure this is a function call
+        $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
+        if ($tokens[$next]['code'] !== T_OPEN_PARENTHESIS) {
+            return;
+        }
+
+        // Get the version parameter position
+        $paramPosition = $this->functions[$functionName];
+
+        // Find the version parameter
+        $parameters = $this->getFunctionParameters($phpcsFile, $stackPtr);
+
+        if (!isset($parameters[$paramPosition - 1])) {
+            return;
+        }
+
+        $versionParam = $parameters[$paramPosition - 1]['raw'];
+        $version = trim(str_replace(["'", '"'], '', $versionParam));
+
+        // Check if it's a valid placeholder
+        if (in_array($version, $this->validPlaceholders, true)) {
+            if ($version !== '@since next-version') {
+                $fix = $phpcsFile->addFixableWarning(
+                    'Please use "@since next-version" instead of "%s"',
+                    $parameters[$paramPosition - 1]['start'],
+                    'OldVersionPlaceholder',
+                    [$version]
+                );
+
+                if ($fix === true) {
+                    return $this->fixVersionParameter($phpcsFile, $parameters[$paramPosition - 1]);
+                }
+            }
+            return;
+        }
+
+        // Validate semver
+        if (!$this->isValidSemver($version)) {
+            $fix = $phpcsFile->addFixableError(
+                'Invalid version "%s" in %s(). Must be a valid semver version or "@since next-version"',
+                $parameters[$paramPosition - 1]['start'],
+                'InvalidVersion',
+                [$version, $functionName]
+            );
+
+            if ($fix === true) {
+                return $this->fixVersionParameter($phpcsFile, $parameters[$paramPosition - 1]);
+            }
+        }
+    }
+
+    /**
+     * Gets information about function parameters.
+     *
+     * @param File $phpcsFile The file being scanned.
+     * @param int  $stackPtr  The position of the current token in the stack.
+     *
+     * @return array Array with parameter info.
+     */
+    private function getFunctionParameters(File $phpcsFile, $stackPtr)
+    {
+        $tokens = $phpcsFile->getTokens();
+
+        $opener = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $stackPtr);
+        if ($opener === false) {
+            return [];
+        }
+
+        if (!isset($tokens[$opener]['parenthesis_closer'])) {
+            return [];
+        }
+
+        $closer = $tokens[$opener]['parenthesis_closer'];
+        $parameters = [];
+        $nestingLevel = 0;
+        $currentParam = [
+            'start' => null,
+            'end' => null,
+            'raw' => '',
+        ];
+
+        for ($i = $opener + 1; $i < $closer; $i++) {
+            // Track nesting of parentheses
+            if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) {
+                $nestingLevel++;
+            }
+            if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
+                $nestingLevel--;
+            }
+
+            // Only process commas at the base nesting level
+            if ($tokens[$i]['code'] === T_COMMA && $nestingLevel === 0) {
+                $currentParam['end'] = $i - 1;
+                $parameters[] = $currentParam;
+                $currentParam = [
+                    'start' => null,
+                    'end' => null,
+                    'raw' => '',
+                ];
+                continue;
+            }
+
+            if ($currentParam['start'] === null) {
+                $currentParam['start'] = $i;
+            }
+            $currentParam['raw'] .= $tokens[$i]['content'];
+        }
+
+        // Add the last parameter
+        if ($currentParam['start'] !== null) {
+            $currentParam['end'] = $closer - 1;
+            $parameters[] = $currentParam;
+        }
+
+        return $parameters;
+    }
+
+    /**
+     * Check if a version string is valid semver.
+     *
+     * @param string $version Version string to check.
+     *
+     * @return bool
+     */
+    private function isValidSemver($version)
+    {
+        try {
+            $this->versionParser->normalize($version);
+            return true;
+        } catch (\UnexpectedValueException $e) {
+            return false;
+        }
+    }
+
+    private function fixVersionParameter(File $phpcsFile, array $parameter)
+    {
+        $tokens = $phpcsFile->getTokens();
+
+        // Get the full parameter content
+        $content = '';
+        for ($i = $parameter['start']; $i <= $parameter['end']; $i++) {
+            $content .= $tokens[$i]['content'];
+        }
+
+        $phpcsFile->fixer->beginChangeset();
+        $phpcsFile->fixer->replaceToken($parameter['start'], "'@since next-version'");
+
+        // Clear any remaining tokens
+        for ($i = $parameter['start'] + 1; $i <= $parameter['end']; $i++) {
+            $phpcsFile->fixer->replaceToken($i, '');
+        }
+
+        $phpcsFile->fixer->endChangeset();
+
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/phpcs/WPGraphQL/PHPCS/ruleset.xml b/phpcs/WPGraphQL/PHPCS/ruleset.xml
new file mode 100644
index 000000000..3b6c6faa8
--- /dev/null
+++ b/phpcs/WPGraphQL/PHPCS/ruleset.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<ruleset name="WPGraphQL" namespace="WPGraphQL\PHPCS">
+    <description>Custom PHPCS rules for WPGraphQL</description>
+
+    <!-- Define our sniff directory -->
+    <config name="installed_paths" value="../../"/>
+
+    <!-- Register our sniffs -->
+    <rule ref="./Sniffs/Functions/VersionParameterSniff.php"/>
+    <rule ref="./Sniffs/Commenting/ValidSinceTagSniff.php"/>
+</ruleset>
\ No newline at end of file
diff --git a/readme.txt b/readme.txt
index e9c1c7299..75fe968d2 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: GraphQL, Headless, REST API, Decoupled, React
 Requires at least: 6.0
 Tested up to: 6.7.1
 Requires PHP: 7.4
-Stable tag: 2.0.0
+Stable tag: 2.1.0
 License: GPL-3
 License URI: https://www.gnu.org/licenses/gpl-3.0.html
 
@@ -284,6 +284,18 @@ Composer dependencies are no longer versioned in Github. Recommended install sou
 
 == Changelog ==
 
+= 2.1.0 =
+
+**New Features**
+
+- [#3320](https://github.com/wp-graphql/wp-graphql/pull/3320): feat: add filter to Request::is_valid_http_content_type to allow for custom content types with POST method requests
+**Chores / Bugfixes**
+
+- [#3314](https://github.com/wp-graphql/wp-graphql/pull/3314): fix: use version_compare to simplify incompatible dependent check
+- [#3316](https://github.com/wp-graphql/wp-graphql/pull/3316): docs: update changelog and upgrade notice
+- [#3325](https://github.com/wp-graphql/wp-graphql/pull/3325): docs: update quick-start.md
+- [#3190](https://github.com/wp-graphql/wp-graphql/pull/3190): docs: add developer docs for `AbstractConnectionResolver`
+
 = 2.0.0 =
 
 **BREAKING CHANGE UPDATE**
diff --git a/scripts/changelog-formatters/changelog-md.js b/scripts/changelog-formatters/changelog-md.js
new file mode 100644
index 000000000..6130d15e5
--- /dev/null
+++ b/scripts/changelog-formatters/changelog-md.js
@@ -0,0 +1,127 @@
+const path = require('path');
+const fs = require('fs');
+
+/**
+ * Group changes by their type
+ */
+function groupChanges(changesets) {
+    const groups = {
+        breaking: [],
+        features: [],
+        fixes: [],
+        docs: [],
+        other: []
+    };
+
+    changesets.forEach(changeset => {
+        if (changeset.breaking || changeset.breaking_changes) {
+            groups.breaking.push(changeset);
+            return;
+        }
+
+        if (changeset.summary.startsWith('feat:')) {
+            groups.features.push(changeset);
+        } else if (changeset.summary.startsWith('fix:')) {
+            groups.fixes.push(changeset);
+        } else if (changeset.summary.startsWith('docs:')) {
+            groups.docs.push(changeset);
+        } else {
+            groups.other.push(changeset);
+        }
+    });
+
+    return groups;
+}
+
+/**
+ * Format a PR link
+ */
+function formatPRLink(pr_number) {
+    return `[#${pr_number}](https://github.com/wp-graphql/wp-graphql/pull/${pr_number})`;
+}
+
+/**
+ * Format a single change entry
+ */
+function formatChangeEntry(changeset) {
+    const summary = changeset.summary.split(':')[1].trim();
+    return `- ${summary} (${formatPRLink(changeset.pr_number)})`;
+}
+
+/**
+ * Format breaking changes section
+ */
+function formatBreakingChanges(changesets) {
+    if (!changesets.length) return '';
+
+    let content = '### ⚠ BREAKING CHANGES\n\n';
+
+    changesets.forEach(changeset => {
+        content += `${changeset.breaking_changes}\n\n`;
+        if (changeset.upgrade_instructions) {
+            content += '#### Upgrade Instructions\n\n';
+            content += `${changeset.upgrade_instructions}\n\n`;
+        }
+        content += `(${formatPRLink(changeset.pr_number)})\n\n`;
+    });
+
+    return content;
+}
+
+/**
+ * Format changes section
+ */
+function formatChangesSection(title, changes) {
+    if (!changes.length) return '';
+
+    return `### ${title}\n\n${changes.map(formatChangeEntry).join('\n')}\n\n`;
+}
+
+/**
+ * Format the complete changelog for CHANGELOG.md
+ */
+function formatChangelogMd(version, changesets) {
+    const groups = groupChanges(changesets);
+    let content = `## [${version}]\n\n`;
+
+    // Add breaking changes first if any
+    if (groups.breaking.length) {
+        content += formatBreakingChanges(groups.breaking);
+    }
+
+    // Add other changes grouped by type
+    content += formatChangesSection('Features', groups.features);
+    content += formatChangesSection('Bug Fixes', groups.fixes);
+    content += formatChangesSection('Documentation', groups.docs);
+
+    if (groups.other.length) {
+        content += formatChangesSection('Other Changes', groups.other);
+    }
+
+    return content;
+}
+
+/**
+ * Update CHANGELOG.md with new release
+ */
+async function updateChangelogMd(release, options) {
+    const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
+    let content = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, 'utf8') : '# Changelog\n\n';
+
+    const newContent = formatChangelogMd(release.newVersion, release.changesets);
+
+    // Insert after the first line (title)
+    const lines = content.split('\n');
+    lines.splice(2, 0, newContent);
+    content = lines.join('\n');
+
+    fs.writeFileSync(changelogPath, content);
+}
+
+module.exports = {
+    formatChangelogMd,
+    groupChanges, // Export for testing
+    formatPRLink, // Export for testing
+    formatChangeEntry, // Export for testing
+    updateChangelogMd
+};
\ No newline at end of file
diff --git a/scripts/changelog-formatters/readme-txt.js b/scripts/changelog-formatters/readme-txt.js
new file mode 100644
index 000000000..49e513132
--- /dev/null
+++ b/scripts/changelog-formatters/readme-txt.js
@@ -0,0 +1,328 @@
+const { getInfo } = require('@changesets/get-github-info');
+const fs = require('fs');
+const path = require('path');
+
+/**
+ * Updates the stable tag in readme.txt for stable releases
+ */
+function updateStableTag(version) {
+    const readmePath = path.join(process.cwd(), 'readme.txt');
+    let content = fs.readFileSync(readmePath, 'utf8');
+
+    // Only update stable tag for stable releases (no pre-release suffix)
+    if (!version.includes('-')) {
+        content = content.replace(
+            /Stable tag: .+/,
+            `Stable tag: ${version}`
+        );
+        fs.writeFileSync(readmePath, content);
+    }
+}
+
+/**
+ * Generate upgrade notice entry for readme.txt
+ */
+async function getUpgradeNoticeEntry(release, options) {
+    const { newVersion, changesets } = release;
+
+    // Skip upgrade notices for patch versions without breaking changes
+    if (newVersion.match(/\d+\.\d+\.\d+$/) && !changesets.some(c => c.breaking)) {
+        // Only skip if it's not a minor version (x.y.0)
+        if (!newVersion.endsWith('.0')) {
+            return '';
+        }
+    }
+
+    let notice = `= ${newVersion} =\n`;
+
+    // Check for breaking changes
+    const breakingChanges = changesets.filter(c => c.breaking);
+    if (breakingChanges.length > 0) {
+        notice += '**BREAKING CHANGE UPDATE**\n\n';
+        for (const changeset of breakingChanges) {
+            if (changeset.breaking_changes) {
+                notice += changeset.breaking_changes + '\n\n';
+            }
+            if (changeset.pr_number) {
+                notice += `In <a href="${options.repo}/pull/${changeset.pr_number}">#${changeset.pr_number}</a>\n\n`;
+            }
+            if (changeset.upgrade_instructions) {
+                notice += changeset.upgrade_instructions + '\n\n';
+            }
+        }
+    } else if (newVersion.match(/\d+\.\d+\.0$/)) {
+        // Standard notice for minor versions
+        notice += 'While there are no known breaking changes in this release, as this is a minor version update, we recommend testing on staging servers before updating production environments.\n';
+    }
+
+    return notice.trim() + '\n';
+}
+
+/**
+ * Updates readme.txt with changelog and upgrade notice
+ */
+async function updateReadmeTxt(release, options) {
+    const readmePath = path.join(process.cwd(), 'readme.txt');
+    let content = fs.readFileSync(readmePath, 'utf8');
+
+    // Generate changelog
+    const changelog = formatReadmeTxt(release.newVersion, release.changesets);
+
+    // Update changelog section
+    content = content.replace(
+        /(== Changelog ==\n\n)/,
+        `$1${changelog}`
+    );
+
+    // Generate upgrade notice if needed
+    const upgradeNotice = await getUpgradeNoticeEntry(release, options);
+    if (upgradeNotice) {
+        if (content.includes('== Upgrade Notice ==')) {
+            content = content.replace(
+                /(== Upgrade Notice ==\n\n)/,
+                `$1${upgradeNotice}\n\n`
+            );
+        } else {
+            content += `\n\n== Upgrade Notice ==\n\n${upgradeNotice}\n`;
+        }
+    }
+
+    // Update stable tag if needed
+    if (!release.newVersion.includes('-')) {
+        updateStableTag(release.newVersion);
+    }
+
+    fs.writeFileSync(readmePath, content);
+}
+
+/**
+ * Formats changelog entries for WordPress.org readme.txt
+ */
+async function getReadmeTxtReleaseLine(changeset, type, options) {
+    if (!changeset || !changeset.summary) {
+        return '';
+    }
+
+    const [firstLine] = changeset.summary.split('\n');
+    let links;
+    try {
+        if (process.env.GITHUB_TOKEN) {
+            const info = await getInfo({
+                repo: options.repo,
+                commit: changeset.commit
+            });
+            links = info.links;
+        }
+    } catch (error) {
+        console.warn('Warning: Could not fetch GitHub info. PR links will not be included.');
+    }
+
+    return `* ${firstLine}${links ? ` (${links.pull})` : ''}`;
+}
+
+/**
+ * Groups changes by type (Features/Bugfixes)
+ */
+function getDependencyReleaseLine() {
+    return '';
+}
+
+async function getReadmeTxtChangelog(release, options) {
+    const heading = `= ${release.newVersion} =\n`;
+
+    // Update stable tag for stable releases
+    updateStableTag(release.newVersion);
+
+    const features = [];
+    const bugfixes = [];
+    const other = [];
+
+    for (const changeset of release.changesets) {
+        const line = await getReadmeTxtReleaseLine(changeset, 'patch', options);
+
+        // Skip empty lines
+        if (!line) continue;
+
+        // Only try to categorize if summary exists
+        if (changeset && changeset.summary) {
+            if (changeset.summary.startsWith('feat')) {
+                features.push(line);
+            } else if (changeset.summary.startsWith('fix')) {
+                bugfixes.push(line);
+            } else {
+                other.push(line);
+            }
+        } else {
+            // If no summary, add to other changes
+            other.push(line);
+        }
+    }
+
+    const sections = [];
+
+    if (features.length) {
+        sections.push('**New Features**\n\n' + features.join('\n'));
+    }
+
+    if (bugfixes.length) {
+        sections.push('**Chores / Bugfixes**\n\n' + bugfixes.join('\n'));
+    }
+
+    if (other.length) {
+        sections.push('**Other Changes**\n\n' + other.join('\n'));
+    }
+
+    return heading + '\n' + sections.join('\n\n') + '\n\n';
+}
+
+/**
+ * Group changes by their type for readme.txt
+ */
+function groupChanges(changesets) {
+    const groups = {
+        breaking: [],
+        features: [],
+        fixes: [],
+        other: []
+    };
+
+    changesets.forEach(changeset => {
+        if (changeset.breaking || changeset.breaking_changes) {
+            groups.breaking.push(changeset);
+            return;
+        }
+
+        // Skip documentation changes in readme.txt
+        if (changeset.summary.startsWith('docs:')) {
+            return;
+        }
+
+        if (changeset.summary.startsWith('feat:')) {
+            groups.features.push(changeset);
+        } else if (changeset.summary.startsWith('fix:')) {
+            groups.fixes.push(changeset);
+        } else {
+            groups.other.push(changeset);
+        }
+    });
+
+    return groups;
+}
+
+/**
+ * Format a single change entry for readme.txt
+ */
+function formatChangeEntry(changeset) {
+    const summary = changeset.summary.split(':')[1].trim();
+    let entry = `* ${summary}`;
+
+    // Add PR link if available and GITHUB_TOKEN exists
+    if (process.env.GITHUB_TOKEN && changeset.pr_number) {
+        entry += ` (<a href="https://github.com/wp-graphql/wp-graphql/pull/${changeset.pr_number}">#${changeset.pr_number}</a>)`;
+    }
+
+    return entry;
+}
+
+/**
+ * Format breaking changes section for readme.txt
+ */
+function formatBreakingChanges(changesets) {
+    if (!changesets.length) return '';
+
+    let content = '**BREAKING CHANGES**\n\n';
+
+    changesets.forEach(changeset => {
+        content += `${changeset.breaking_changes}\n\n`;
+        if (changeset.upgrade_instructions) {
+            content += 'Upgrade Instructions:\n';
+            content += `${changeset.upgrade_instructions}\n\n`;
+        }
+        if (process.env.GITHUB_TOKEN && changeset.pr_number) {
+            content += `See <a href="https://github.com/wp-graphql/wp-graphql/pull/${changeset.pr_number}">#${changeset.pr_number}</a> for details.\n\n`;
+        }
+    });
+
+    return content;
+}
+
+/**
+ * Format changes section for readme.txt
+ */
+function formatChangesSection(title, changes) {
+    if (!changes.length) return '';
+
+    return `**${title}**\n\n${changes.map(formatChangeEntry).join('\n')}\n\n`;
+}
+
+/**
+ * Format upgrade notice for readme.txt
+ */
+function formatUpgradeNotice(version, changesets) {
+    const breakingChanges = changesets.filter(c => c.breaking || c.breaking_changes);
+
+    if (!breakingChanges.length) {
+        // For minor versions, add a standard notice
+        if (version.match(/\d+\.\d+\.0$/)) {
+            return `= ${version} =\n` +
+                   'While there are no breaking changes in this release, as this is a minor version update, ' +
+                   'we recommend testing on staging servers before updating production environments.\n\n';
+        }
+        return '';
+    }
+
+    let notice = `= ${version} =\n**BREAKING CHANGE UPDATE**\n\n`;
+
+    breakingChanges.forEach(changeset => {
+        notice += `${changeset.breaking_changes}\n\n`;
+        if (changeset.upgrade_instructions) {
+            notice += `${changeset.upgrade_instructions}\n\n`;
+        }
+        if (process.env.GITHUB_TOKEN && changeset.pr_number) {
+            notice += `See <a href="https://github.com/wp-graphql/wp-graphql/pull/${changeset.pr_number}">#${changeset.pr_number}</a> for details.\n\n`;
+        }
+    });
+
+    return notice;
+}
+
+/**
+ * Format the complete changelog for readme.txt
+ */
+function formatReadmeTxt(version, changesets) {
+    const groups = groupChanges(changesets);
+    let content = `= ${version} =\n\n`;
+
+    // Add breaking changes first if any
+    if (groups.breaking.length) {
+        content += formatBreakingChanges(groups.breaking);
+    }
+
+    // Add other changes grouped by type
+    content += formatChangesSection('New Features', groups.features);
+    content += formatChangesSection('Bug Fixes', groups.fixes);
+
+    if (groups.other.length) {
+        content += formatChangesSection('Other Changes', groups.other);
+    }
+
+    // Add upgrade notice if needed
+    const upgradeNotice = formatUpgradeNotice(version, changesets);
+    if (upgradeNotice) {
+        content += '\n== Upgrade Notice ==\n\n' + upgradeNotice;
+    }
+
+    return content;
+}
+
+module.exports = {
+    getReadmeTxtChangelog,
+    getDependencyReleaseLine,
+    updateStableTag,
+    getUpgradeNoticeEntry,
+    updateReadmeTxt,
+    formatReadmeTxt,
+    groupChanges,
+    formatChangeEntry,
+    formatUpgradeNotice
+};
\ No newline at end of file
diff --git a/scripts/check-versions.js b/scripts/check-versions.js
new file mode 100644
index 000000000..924af61a9
--- /dev/null
+++ b/scripts/check-versions.js
@@ -0,0 +1,68 @@
+const { getCurrentVersions } = require('./version-management');
+
+async function checkVersions() {
+    // Dynamic import of chalk
+    const { default: chalk } = await import('chalk');
+
+    try {
+        const versions = getCurrentVersions();
+
+        console.log('\nCurrent versions across files:');
+        console.log('=============================');
+
+        // Display versions
+        Object.entries(versions).forEach(([file, version]) => {
+            const color = version ? chalk.green : chalk.red;
+            console.log(`${chalk.bold(file.padEnd(12))}: ${color(version || 'NOT FOUND')}`);
+        });
+
+        // Check for mismatches
+        const uniqueVersions = new Set(Object.values(versions).filter(Boolean));
+
+        console.log('\nValidation:');
+        console.log('===========');
+
+        if (uniqueVersions.size > 1) {
+            console.log(chalk.red('❌ Version mismatch detected!'));
+            return process.exit(1);
+        }
+
+        // Check if any versions are missing
+        const missingVersions = Object.entries(versions)
+            .filter(([_, version]) => !version)
+            .map(([file]) => file);
+
+        if (missingVersions.length > 0) {
+            console.log(chalk.red(`❌ Missing versions in: ${missingVersions.join(', ')}`));
+            return process.exit(1);
+        }
+
+        console.log(chalk.green('✓ All versions match:'), chalk.bold([...uniqueVersions][0]));
+
+        // Check if current version is beta
+        if ([...uniqueVersions][0].includes('-beta')) {
+            console.log(chalk.yellow('\nNote: Current version is a beta release'));
+            if (versions.readme === versions.package) {
+                console.log(chalk.red('❌ Warning: Stable tag should not match beta version'));
+                return process.exit(1);
+            }
+            console.log(chalk.green('✓ Stable tag correctly maintained at:', versions.readme));
+        }
+
+        return process.exit(0);
+    } catch (error) {
+        const errorMessage = error.message || 'Unknown error';
+        console.error('\n❌ Error checking versions:', errorMessage);
+        return process.exit(1);
+    }
+}
+
+// Run if called directly
+if (require.main === module) {
+    checkVersions().catch(error => {
+        console.error('Failed to check versions:', error);
+        process.exit(1);
+    });
+}
+
+module.exports = checkVersions;
\ No newline at end of file
diff --git a/scripts/generate-changeset.js b/scripts/generate-changeset.js
new file mode 100644
index 000000000..d19e73918
--- /dev/null
+++ b/scripts/generate-changeset.js
@@ -0,0 +1,170 @@
+const { execSync } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const { generateSinceTagsMetadata } = require('./scan-since-tags');
+
+// Define allowed types
+const ALLOWED_TYPES = ['feat', 'fix', 'chore', 'docs', 'perf', 'refactor', 'revert', 'style', 'test', 'ci', 'build'];
+
+/**
+ * Parse PR title for type and breaking changes
+ */
+function parseTitle(title) {
+    // Match: type(scope)!: description or type!: description
+    const typeMatch = title.match(/^(feat|fix|build|chore|ci|docs|perf|refactor|revert|style|test)(?:\([^)]+\))?(!)?:/);
+    if (!typeMatch) {
+        throw new Error(`PR title does not follow conventional commit format. Must start with type: ${ALLOWED_TYPES.join(', ')}`);
+    }
+
+    const type = typeMatch[1];
+    const isBreaking = Boolean(typeMatch[2] || title.includes('BREAKING CHANGE'));
+
+    if (!ALLOWED_TYPES.includes(type)) {
+        throw new Error(`Invalid type "${type}". Must be one of: ${ALLOWED_TYPES.join(', ')}`);
+    }
+
+    return {
+        type,
+        isBreaking
+    };
+}
+
+/**
+ * Format the summary with type prefix
+ */
+function formatSummary(type, isBreaking, description) {
+    return `${type}${isBreaking ? '!' : ''}: ${description.trim()}`;
+}
+
+/**
+ * Extract sections from PR body
+ */
+function parsePRBody(body) {
+    const sections = {
+        description: body.match(/What does this implement\/fix\? Explain your changes\.\s*-+\s*([\s\S]*?)(?=##|$)/)?.[1]?.trim() || '',
+        breaking: body.match(/#{2,3}\s*Breaking Changes\s*([\s\S]*?)(?=##|$)/)?.[1]?.trim() || '',
+        upgrade: body.match(/#{2,3}\s*Upgrade Instructions\s*([\s\S]*?)(?=##|$)/)?.[1]?.trim() || ''
+    };
+
+    // Clean up any "N/A" or similar placeholders
+    Object.keys(sections).forEach(key => {
+        if (sections[key].toLowerCase() === 'n/a' || sections[key].toLowerCase() === 'none') {
+            sections[key] = '';
+        }
+    });
+
+    return sections;
+}
+
+/**
+ * Create changeset using @changesets/cli
+ */
+async function createChangeset({ title, body, prNumber }) {
+    // Parse PR title and body
+    const { type, isBreaking } = parseTitle(title);
+    const sections = parsePRBody(body);
+
+    // Validate breaking changes have upgrade instructions
+    if (isBreaking || sections.breaking) {
+        if (!sections.breaking) {
+            throw new Error('Breaking changes must be documented in the PR description');
+        }
+        if (!sections.upgrade) {
+            throw new Error('Breaking changes must include upgrade instructions');
+        }
+    }
+
+    // Get description from PR body or fallback to title
+    const description = sections.description || title.split(':')[1].trim();
+
+    // Format the summary
+    const summary = formatSummary(type, isBreaking, description);
+
+    // Determine bump type
+    const bumpType = isBreaking || sections.breaking ? 'major' : (type === 'feat' ? 'minor' : 'patch');
+
+    // Create the changeset content
+    const changesetContent = {
+        summary,
+        type: bumpType,
+        pr: Number(prNumber),
+        pr_number: prNumber,
+        pr_url: `https://github.com/wp-graphql/wp-graphql/pull/${prNumber}`,
+        breaking: isBreaking,
+        breaking_changes: sections.breaking,
+        upgrade_instructions: sections.upgrade,
+        releases: [{ name: 'wp-graphql', type: bumpType }]
+    };
+
+    // Get @since tags metadata
+    const sinceMetadata = await generateSinceTagsMetadata();
+
+    // Create a unique ID for the changeset
+    const changesetId = `pr-${prNumber}-${Date.now()}`;
+    const changesetDir = path.join(process.cwd(), '.changeset');
+    const changesetPath = path.join(changesetDir, `${changesetId}.md`);
+
+    // Ensure .changeset directory exists
+    if (!fs.existsSync(changesetDir)) {
+        fs.mkdirSync(changesetDir, { recursive: true });
+    }
+
+    // Create the changeset file content
+    let fileContent = '---\n';
+    fileContent += JSON.stringify(changesetContent, null, 2);
+    fileContent += '\n---\n\n';
+    fileContent += summary;
+
+    if (sections.breaking) {
+        fileContent += '\n\nBREAKING CHANGES:\n' + sections.breaking;
+    }
+
+    if (sections.upgrade) {
+        fileContent += '\n\nUPGRADE INSTRUCTIONS:\n' + sections.upgrade;
+    }
+
+    if (sinceMetadata.sinceFiles.length > 0) {
+        fileContent += '\n\nFILES WITH @since TAGS TO UPDATE:\n' + sinceMetadata.sinceFiles.join('\n');
+    }
+
+    // Write the changeset file
+    fs.writeFileSync(changesetPath, fileContent);
+
+    return {
+        type: bumpType,
+        breaking: isBreaking,
+        pr: Number(prNumber),
+        sinceFiles: sinceMetadata.sinceFiles,
+        totalSinceTags: sinceMetadata.totalTags,
+        changesetId
+    };
+}
+
+// When run directly from command line
+if (require.main === module) {
+    const title = process.env.PR_TITLE;
+    const body = process.env.PR_BODY;
+    const prNumber = process.env.PR_NUMBER;
+
+    if (!title || !body || !prNumber) {
+        console.error('Missing required environment variables');
+        process.exit(1);
+    }
+
+    createChangeset({ title, body, prNumber })
+        .then(result => {
+            console.log('Changeset created successfully:', result);
+        })
+        .catch(error => {
+            console.error('Error creating changeset:', error);
+            process.exit(1);
+        });
+}
+
+module.exports = {
+    parseTitle,
+    parsePRBody,
+    createChangeset,
+    formatSummary,
+    ALLOWED_TYPES
+};
\ No newline at end of file
diff --git a/scripts/release-version.js b/scripts/release-version.js
new file mode 100644
index 000000000..8d1c8b69c
--- /dev/null
+++ b/scripts/release-version.js
@@ -0,0 +1,1 @@
+ 
diff --git a/scripts/scan-since-tags.js b/scripts/scan-since-tags.js
new file mode 100644
index 000000000..37714c56a
--- /dev/null
+++ b/scripts/scan-since-tags.js
@@ -0,0 +1,77 @@
+const fs = require('fs');
+const { glob } = require('glob');
+
+/**
+ * Scan file for @since todo tags
+ */
+function scanFileForSinceTags(filePath) {
+    try {
+        const content = fs.readFileSync(filePath, 'utf8');
+        const regex = /@since\s+(todo|next-version|tbd)|@next-version/g;
+        const matches = content.match(regex);
+        return matches ? matches.length : 0;
+    } catch (error) {
+        console.error(`Error scanning ${filePath}:`, error.message);
+        return 0;
+    }
+}
+
+/**
+ * Find all files with @since todo tags
+ */
+async function findFilesWithSinceTags(pattern = 'src/**/*.php') {
+    try {
+        const files = await glob(pattern);
+        const results = [];
+
+        for (const file of files) {
+            const count = scanFileForSinceTags(file);
+            if (count > 0) {
+                results.push({ file, count });
+            }
+        }
+
+        return results;
+    } catch (error) {
+        console.error('Error finding files:', error);
+        return [];
+    }
+}
+
+/**
+ * Generate metadata about files containing @since tags
+ */
+async function generateSinceTagsMetadata() {
+    try {
+        const files = await findFilesWithSinceTags();
+        const metadata = {
+            sinceFiles: files.map(({ file }) => file),
+            totalTags: files.reduce((sum, { count }) => sum + count, 0)
+        };
+
+        if (files.length > 0) {
+            console.log('\nFound files with @since tags to update:');
+            files.forEach(({ file, count }) => {
+                console.log(`- ${file} (${count} tags)`);
+            });
+        } else {
+            console.log('\nNo files found with @since tags to update');
+        }
+
+        console.log(`Total tags to update: ${metadata.totalTags}\n`);
+
+        return metadata;
+    } catch (error) {
+        console.error('Error generating metadata:', error);
+        return {
+            sinceFiles: [],
+            totalTags: 0
+        };
+    }
+}
+
+module.exports = {
+    scanFileForSinceTags,
+    findFilesWithSinceTags,
+    generateSinceTagsMetadata
+};
\ No newline at end of file
diff --git a/scripts/simulate-release.js b/scripts/simulate-release.js
new file mode 100644
index 000000000..00345adf1
--- /dev/null
+++ b/scripts/simulate-release.js
@@ -0,0 +1,85 @@
+require('dotenv').config();
+const { getCurrentVersions, updateVersions } = require('./version-management');
+const { updateAllSinceTags } = require('./update-since-tags');
+const { getReadmeTxtChangelog } = require('./changelog-formatters/readme-txt');
+const chalk = require('chalk');
+
+async function simulateRelease(newVersion, options = { beta: false }) {
+    try {
+        // 1. Check current versions
+        console.log(chalk.blue('\n1. Checking current versions...'));
+        const currentVersions = getCurrentVersions();
+        console.log('Current versions:', currentVersions);
+
+        // 2. Update versions
+        console.log(chalk.blue('\n2. Updating version numbers...'));
+        await updateVersions(newVersion, options.beta);
+        console.log(chalk.green('✓ Version numbers updated'));
+
+        // 3. Update @since tags
+        console.log(chalk.blue('\n3. Updating @since todo tags...'));
+        const sinceResults = await updateAllSinceTags(newVersion);
+        if (sinceResults.updated.length > 0) {
+            console.log(chalk.green(`✓ Updated ${sinceResults.updated.length} files with @since tags`));
+        } else {
+            console.log(chalk.yellow('No @since todo tags found'));
+        }
+
+        // 4. Generate changelog
+        console.log(chalk.blue('\n4. Generating changelog entries...'));
+        const mockRelease = {
+            newVersion,
+            changesets: [
+                // Mock changeset for testing
+                {
+                    summary: 'feat: Test feature',
+                    commit: 'test-commit'
+                }
+            ]
+        };
+        const changelog = await getReadmeTxtChangelog(mockRelease, {
+            repo: 'wp-graphql/wp-graphql'
+        });
+        console.log('Changelog preview:', changelog);
+
+        // 5. Final validation
+        console.log(chalk.blue('\n5. Validating final state...'));
+        const finalVersions = getCurrentVersions();
+        console.log('Final versions:', finalVersions);
+
+        console.log(chalk.green('\n✓ Release simulation completed successfully!'));
+        return true;
+    } catch (error) {
+        console.error(chalk.red('\n❌ Release simulation failed:'), error.message);
+        throw error;
+    }
+}
+
+// CLI interface
+async function main() {
+    try {
+        const version = process.argv[2];
+        const isBeta = process.argv.includes('--beta');
+
+        if (!version) {
+            throw new Error('Version argument is required. Usage: npm run simulate-release <version> [--beta]');
+        }
+
+        // Check for GitHub token
+        if (!process.env.GITHUB_TOKEN) {
+            console.log(chalk.yellow('\nWarning: No GITHUB_TOKEN found in environment. PR links will not be included in changelog.'));
+        }
+
+        await simulateRelease(version, { beta: isBeta });
+    } catch (error) {
+        console.error(error.message);
+        process.exit(1);
+    }
+}
+
+// Run if called directly
+if (require.main === module) {
+    main();
+}
+
+module.exports = simulateRelease;
\ No newline at end of file
diff --git a/scripts/simulate-workflow.js b/scripts/simulate-workflow.js
new file mode 100644
index 000000000..f29c8d129
--- /dev/null
+++ b/scripts/simulate-workflow.js
@@ -0,0 +1,451 @@
+#!/usr/bin/env node
+
+const { createChangeset } = require('./generate-changeset');
+const { updateVersions, getCurrentVersions, resetVersions } = require('./version-management');
+const { updateAllSinceTags, findSinceTodoFiles, getSincePlaceholders } = require('./update-since-tags');
+const { formatChangelogMd } = require('./changelog-formatters/changelog-md');
+const { formatReadmeTxt } = require('./changelog-formatters/readme-txt');
+const fs = require('fs');
+const path = require('path');
+
+// Initialize chalk at the start
+let chalk;
+
+/**
+ * Initialize chalk
+ */
+async function initChalk() {
+    const { default: chalkModule } = await import('chalk');
+    chalk = chalkModule;
+}
+
+/**
+ * Create test changesets for simulation
+ */
+function createTestChangesets() {
+    return [
+        {
+            summary: 'feat: Add new GraphQL field',
+            type: 'minor',
+            pr: 123,
+            pr_number: '123',
+            pr_url: 'https://github.com/wp-graphql/wp-graphql/pull/123',
+            breaking: false,
+            breaking_changes: '',
+            upgrade_instructions: '',
+            releases: [{ name: 'wp-graphql', type: 'minor' }]
+        },
+        {
+            summary: 'feat!: Breaking API change',
+            type: 'major',
+            pr: 124,
+            pr_number: '124',
+            pr_url: 'https://github.com/wp-graphql/wp-graphql/pull/124',
+            breaking: true,
+            breaking_changes: 'This changes the way mutations handle input validation',
+            upgrade_instructions: 'Update your mutation input to include the new required fields',
+            releases: [{ name: 'wp-graphql', type: 'major' }]
+        },
+        {
+            summary: 'fix: Resolve N+1 query issue',
+            type: 'patch',
+            pr: 125,
+            pr_number: '125',
+            pr_url: 'https://github.com/wp-graphql/wp-graphql/pull/125',
+            breaking: false,
+            breaking_changes: '',
+            upgrade_instructions: '',
+            releases: [{ name: 'wp-graphql', type: 'patch' }]
+        },
+        {
+            summary: 'docs: Update API documentation',
+            type: 'patch',
+            pr: 126,
+            pr_number: '126',
+            pr_url: 'https://github.com/wp-graphql/wp-graphql/pull/126',
+            breaking: false,
+            breaking_changes: '',
+            upgrade_instructions: '',
+            releases: [{ name: 'wp-graphql', type: 'patch' }]
+        }
+    ];
+}
+
+/**
+ * Calculate next version based on current version and changesets
+ */
+function calculateNextVersion(currentVersion, changesets) {
+    const [major, minor, patch] = currentVersion.split('.').map(Number);
+
+    // Check for breaking changes
+    const hasBreakingChanges = changesets.some(
+        changeset => changeset.breaking || changeset.breaking_changes || changeset.type === 'major'
+    );
+    if (hasBreakingChanges) {
+        return `${major + 1}.0.0`;
+    }
+
+    // Check for features (minor changes)
+    const hasFeatures = changesets.some(
+        changeset => changeset.type === 'minor' || changeset.summary.startsWith('feat:')
+    );
+    if (hasFeatures) {
+        return `${major}.${minor + 1}.0`;
+    }
+
+    // Otherwise, it's a patch
+    return `${major}.${minor}.${patch + 1}`;
+}
+
+/**
+ * Get the next version number
+ */
+function getNextVersion(currentVersion, forceVersion = false) {
+    if (forceVersion) {
+        // For forced version, increment the patch number
+        const [major, minor, patch] = currentVersion.split('.').map(Number);
+        return `${major}.${minor}.${patch + 1}`;
+    }
+
+    // Read changesets and calculate version based on changes
+    const changesets = fs.existsSync('.changeset')
+        ? fs.readdirSync('.changeset').filter(file => file.endsWith('.md') && file !== 'README.md')
+        : [];
+
+    if (changesets.length === 0) {
+        // No changes, increment patch
+        const [major, minor, patch] = currentVersion.split('.').map(Number);
+        return `${major}.${minor}.${patch + 1}`;
+    }
+
+    return calculateNextVersion(currentVersion, changesets);
+}
+
+/**
+ * Simulate changelog generation
+ */
+async function simulateChangelog(version, options = {}) {
+    // Get current version
+    const currentVersions = getCurrentVersions();
+    const currentVersion = currentVersions.package;
+
+    // Get changesets (either from test data or actual .changeset directory)
+    const changesets = options.useTestData ? createTestChangesets() : await readActualChangesets();
+
+    // Calculate next version if not provided
+    const nextVersion = version || calculateNextVersion(currentVersion, changesets);
+
+    console.log(chalk.blue('\nSimulating Changelog Generation:'));
+    console.log('Current Version:', currentVersion);
+    console.log('Next Version:', nextVersion);
+    console.log('Options:', options);
+
+    try {
+        // Generate CHANGELOG.md content
+        console.log(chalk.blue('\nCHANGELOG.md preview:'));
+        console.log(chalk.yellow('----------------------------------------'));
+        const mdContent = formatChangelogMd(nextVersion, changesets);
+        console.log(mdContent);
+        console.log(chalk.yellow('----------------------------------------'));
+
+        // Generate readme.txt content
+        console.log(chalk.blue('\nreadme.txt preview:'));
+        console.log(chalk.yellow('----------------------------------------'));
+        const txtContent = formatReadmeTxt(nextVersion, changesets);
+        console.log(txtContent);
+        console.log(chalk.yellow('----------------------------------------'));
+
+        // Show what would be updated
+        if (!options.dryRun) {
+            console.log(chalk.blue('\nFiles that would be updated:'));
+            console.log(chalk.gray('- CHANGELOG.md'));
+            console.log(chalk.gray('- readme.txt'));
+
+            if (options.write) {
+                // Actually write the files if --write flag is used
+                const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
+                const readmePath = path.join(process.cwd(), 'readme.txt');
+
+                let changelogContent = fs.existsSync(changelogPath)
+                    ? fs.readFileSync(changelogPath, 'utf8')
+                    : '# Changelog\n\n';
+
+                // Insert new content after the title
+                const lines = changelogContent.split('\n');
+                lines.splice(2, 0, mdContent);
+                fs.writeFileSync(changelogPath, lines.join('\n'));
+
+                // Update readme.txt
+                let readmeContent = fs.readFileSync(readmePath, 'utf8');
+                readmeContent = readmeContent.replace(
+                    /(== Changelog ==\n\n)/,
+                    `$1${txtContent}`
+                );
+                fs.writeFileSync(readmePath, readmeContent);
+
+                console.log(chalk.green('\n✓ Files updated successfully'));
+            }
+        }
+
+        return true;
+    } catch (error) {
+        console.error(chalk.red('\n❌ Error generating changelog:'), error.message);
+        throw error;
+    }
+}
+
+/**
+ * Read actual changesets from the .changeset directory
+ */
+async function readActualChangesets() {
+    const changesetDir = path.join(process.cwd(), '.changeset');
+    const changesets = [];
+
+    if (fs.existsSync(changesetDir)) {
+        const files = fs.readdirSync(changesetDir);
+        for (const file of files) {
+            if (file.endsWith('.md') && file !== 'README.md') {
+                const content = fs.readFileSync(path.join(changesetDir, file), 'utf8');
+                const [, frontmatter] = content.split('---\n');
+                if (frontmatter) {
+                    const changeset = JSON.parse(frontmatter);
+                    changesets.push(changeset);
+                }
+            }
+        }
+    }
+
+    return changesets;
+}
+
+/**
+ * Simulate PR merge and changeset generation
+ */
+async function simulatePRMerge(options = {}) {
+    const pr = {
+        title: options.title || 'feat: Test feature',
+        body: options.body || `What does this implement/fix? Explain your changes.
+---
+This is a test feature
+
+## Breaking Changes
+${options.breaking || ''}
+
+## Upgrade Instructions
+${options.upgrade || ''}`,
+        prNumber: options.prNumber || '999',
+        prUrl: options.prUrl || 'https://github.com/wp-graphql/wp-graphql/pull/999'
+    };
+
+    console.log(chalk.blue('\nSimulating PR Merge:'));
+    console.log('Title:', pr.title);
+    console.log('PR Number:', pr.prNumber);
+
+    try {
+        const result = await createChangeset(pr);
+        console.log(chalk.green('\n✓ Changeset created successfully:'));
+        console.log(JSON.stringify(result, null, 2));
+        return result;
+    } catch (error) {
+        console.error(chalk.red('\n❌ Error creating changeset:'), error.message);
+        throw error;
+    }
+}
+
+/**
+ * Simulate version update
+ */
+async function simulateVersionUpdate() {
+    console.log('\nSimulating Version Update:');
+
+    // Get current version from package.json
+    const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
+    const currentVersion = packageJson.version;
+
+    // Determine next version
+    const forceVersion = process.env.FORCE_VERSION === '1';
+    const nextVersion = getNextVersion(currentVersion, forceVersion);
+
+    console.log('Current Version:', currentVersion);
+    console.log('Next Version:  ', nextVersion);
+    console.log('Version Jump:  ', `${currentVersion} → ${nextVersion}`);
+    console.log('Reason:', forceVersion ? 'Forced version bump' : 'No changes detected');
+
+    console.log('\nScanning for @since tags to update...');
+
+    // Use the functions from update-since-tags.js
+    try {
+        const files = await findSinceTodoFiles();
+        console.log('\nProcessing files for @since tags...');
+
+        const results = [];
+        for (const file of (files || [])) {
+            try {
+                const content = fs.readFileSync(file, 'utf8');
+                const count = getSincePlaceholders(content);
+                if (count > 0) {
+                    results.push({ file, count });
+                    console.log(`Found ${count} @since tags in ${file}`);
+                }
+            } catch (error) {
+                console.error(`Error processing file ${file}:`, error.message);
+            }
+        }
+
+        if (results.length > 0) {
+            console.log('\nFound files with @since tags to update:');
+            results.forEach(({ file, count }) => {
+                console.log(`- ${file} (${count} tags)`);
+            });
+        } else {
+            console.log('\nNo files found with @since tags to update');
+        }
+
+        const totalTags = results.reduce((sum, { count }) => sum + count, 0);
+        console.log(`Total tags to update: ${totalTags}\n`);
+
+    } catch (error) {
+        console.error('Error scanning for @since tags:', error);
+    }
+
+    // Show current versions
+    console.log('\nCurrent versions:');
+    console.log('php:', currentVersion);
+    console.log('constants:', currentVersion);
+    console.log('package:', currentVersion);
+    console.log('readme:', currentVersion);
+
+    // Simulate updating versions
+    console.log('\n✓ Version numbers updated');
+
+    // Show updated versions
+    console.log('\nUpdated versions:');
+    console.log('php:', nextVersion);
+    console.log('constants:', nextVersion);
+    console.log('package:', nextVersion);
+    console.log('readme:', nextVersion);
+}
+
+/**
+ * Get a human-readable reason for the version bump
+ */
+function getVersionBumpReason(changesets) {
+    const breakingChanges = changesets.filter(c => c.breaking || c.breaking_changes || c.type === 'major');
+    const features = changesets.filter(c => c.type === 'minor' || c.summary.startsWith('feat:'));
+    const fixes = changesets.filter(c => c.type === 'patch' || c.summary.startsWith('fix:'));
+
+    if (breakingChanges.length > 0) {
+        return chalk.red(`Major version bump due to ${breakingChanges.length} breaking change(s)`);
+    }
+    if (features.length > 0) {
+        return chalk.yellow(`Minor version bump due to ${features.length} new feature(s)`);
+    }
+    if (fixes.length > 0) {
+        return chalk.green(`Patch version bump due to ${fixes.length} fix(es)`);
+    }
+    return chalk.gray('No changes detected');
+}
+
+/**
+ * CLI interface
+ */
+async function main() {
+    // Initialize chalk first
+    await initChalk();
+
+    const args = process.argv.slice(2);
+    const command = args[0];
+
+    try {
+        switch (command) {
+            case 'pr':
+                await simulatePRMerge({
+                    title: args[1],
+                    prNumber: args[2],
+                    breaking: args[3],
+                    upgrade: args[4]
+                });
+                break;
+
+            case 'version':
+                await simulateVersionUpdate();
+                break;
+
+            case 'changelog':
+                await simulateChangelog(args[1], {
+                    beta: args.includes('--beta'),
+                    dryRun: !args.includes('--write'),
+                    write: args.includes('--write'),
+                    useTestData: args.includes('--test-data')
+                });
+                break;
+
+            case 'reset':
+                const resetVersion = args[1] || '2.1.0';
+                console.log(chalk.blue('\nResetting version files to:', resetVersion));
+                const versions = resetVersions(resetVersion);
+                console.log(chalk.green('\n✓ Files reset successfully'));
+                console.log('\nCurrent versions:');
+                Object.entries(versions).forEach(([file, ver]) => {
+                    console.log(`${file}: ${ver}`);
+                });
+                break;
+
+            default:
+                console.log(`
+Usage:
+  Simulate PR merge:
+    npm run simulate pr "feat: New feature" 123 "Breaking change" "Upgrade steps"
+
+  Simulate version update:
+    npm run simulate version 2.1.0 [--beta]
+
+  Simulate changelog:
+    npm run simulate changelog 2.1.0 [--beta] [--write]
+
+  Reset version files:
+    npm run simulate reset [version]
+
+Examples:
+  # Simulate regular feature PR
+  npm run simulate pr "feat: Add new feature" 123
+
+  # Simulate breaking change PR
+  npm run simulate pr "feat!: Breaking change" 124 "This breaks something" "Follow these steps"
+
+  # Simulate minor version update
+  npm run simulate version 2.1.0
+
+  # Simulate beta release
+  npm run simulate version 2.1.0-beta.1 --beta
+
+  # Preview changelog changes
+  npm run simulate changelog 2.1.0
+
+  # Actually write changelog changes
+  npm run simulate changelog 2.1.0 --write
+
+  # Reset version files to known good state
+  npm run simulate reset [version]
+`);
+                process.exit(1);
+        }
+    } catch (error) {
+        console.error(chalk.red('\nError:'), error.message);
+        process.exit(1);
+    }
+}
+
+// Run if called directly
+if (require.main === module) {
+    main().catch(error => {
+        console.error('Failed to run simulation:', error);
+        process.exit(1);
+    });
+}
+
+module.exports = {
+    simulatePRMerge,
+    simulateVersionUpdate,
+    simulateChangelog
+};
\ No newline at end of file
diff --git a/scripts/update-since-tags.js b/scripts/update-since-tags.js
new file mode 100644
index 000000000..246917308
--- /dev/null
+++ b/scripts/update-since-tags.js
@@ -0,0 +1,173 @@
+const fs = require('fs');
+const path = require('path');
+const { glob } = require('glob');
+
+/**
+ * Find files containing @since todo tags
+ */
+async function findSinceTodoFiles(pattern = 'src/**/*.php') {
+    try {
+        console.log('\nDebug: Current working directory:', process.cwd());
+        console.log('Debug: Looking for files matching pattern:', pattern);
+
+        // Use glob.sync instead of async glob for simpler handling
+        const files = glob.sync(pattern, {
+            ignore: [
+                'node_modules/**',
+                'vendor/**',
+                'phpcs/**',
+                '.github/**',
+                '.wordpress-org/**',
+                'bin/**',
+                'build/**',
+                'docker/**',
+                'img/**',
+                'phpstan/**',
+                'docs/**'
+            ],
+            dot: false, // Ignore dot directories
+            cwd: process.cwd()
+        });
+
+        console.log('Debug: Found files:', files);
+
+        return files || [];
+    } catch (error) {
+        console.error('Error finding files:', error);
+        return [];
+    }
+}
+
+/**
+ * Get all @since placeholders from a file
+ */
+function getSincePlaceholders(content) {
+    // Update regex to match all our valid placeholders
+    const regex = /@since\s+(todo|next-version|tbd)|@next-version/gi;
+    const matches = content.match(regex);
+    return matches ? matches.length : 0;
+}
+
+/**
+ * Update @since placeholders in a file
+ */
+function updateSinceTags(filePath, version) {
+    try {
+        let content = fs.readFileSync(filePath, 'utf8');
+        const originalContent = content;
+
+        // Replace placeholders with the actual version
+        content = content.replace(
+            /@since\s+(todo|tbd|next-version)|@next-version/gi,
+            `@since ${version}`  // Use the provided version instead of 'next-version'
+        );
+
+        // Only write if content changed
+        if (content !== originalContent) {
+            fs.writeFileSync(filePath, content);
+            return true;
+        }
+
+        return false;
+    } catch (error) {
+        throw new Error(`Error updating ${filePath}: ${error.message}`);
+    }
+}
+
+/**
+ * Update all @since todo tags in the project
+ */
+async function updateAllSinceTags(version, pattern = '**/*.php') {
+    const results = {
+        updated: [],
+        errors: []
+    };
+
+    try {
+        const files = await findSinceTodoFiles(pattern);
+        console.log('Debug: Processing files:', files);
+
+        for (const file of files) {
+            try {
+                const content = fs.readFileSync(file, 'utf8');
+                const count = getSincePlaceholders(content);
+                console.log(`Debug: File ${file} has ${count} @since tags`);
+
+                if (count > 0) {
+                    const updated = updateSinceTags(file, version);
+                    if (updated) {
+                        results.updated.push(file);
+                    }
+                }
+            } catch (error) {
+                results.errors.push({ file, error: error.message });
+            }
+        }
+
+        return results;
+    } catch (error) {
+        throw new Error(`Error updating @since tags: ${error.message}`);
+    }
+}
+
+/**
+ * CLI command to update @since tags
+ */
+async function main() {
+    const { default: chalk } = await import('chalk');
+
+    try {
+        const version = process.argv[2];
+        if (!version) {
+            throw new Error('Version argument is required');
+        }
+
+        console.log(chalk.blue('\nUpdating @since todo tags...'));
+        const results = await updateAllSinceTags(version);
+
+        if (results.updated.length > 0) {
+            console.log(chalk.green('\n✓ Updated files:'));
+            results.updated.forEach(file => {
+                console.log(chalk.gray(`  - ${path.relative(process.cwd(), file)}`));
+            });
+        } else {
+            console.log(chalk.yellow('\nNo @since todo tags found'));
+        }
+
+        if (results.errors.length > 0) {
+            console.log(chalk.red('\n❌ Errors:'));
+            results.errors.forEach(({ file, error }) => {
+                console.log(chalk.gray(`  - ${path.relative(process.cwd(), file)}: ${error}`));
+            });
+            process.exit(1);
+        }
+
+        process.exit(0);
+    } catch (error) {
+        console.error(chalk.red('\n❌ Error:'), error.message);
+        process.exit(1);
+    }
+}
+
+// Run if called directly
+if (require.main === module) {
+    main();
+}
+
+/**
+ * Get count of @since todo tags in content
+ */
+function getSinceTodoTags(content) {
+    // Update to match all non-standard placeholders
+    const regex = /@since\s+(todo|tbd)|@next-version/gi;
+    const matches = content.match(regex);
+    return matches ? matches.length : 0;
+}
+
+module.exports = {
+    findSinceTodoFiles,
+    getSincePlaceholders,
+    updateSinceTags,
+    updateAllSinceTags,
+    getSinceTodoTags
+};
\ No newline at end of file
diff --git a/scripts/version-management.js b/scripts/version-management.js
new file mode 100644
index 000000000..d814c7c13
--- /dev/null
+++ b/scripts/version-management.js
@@ -0,0 +1,255 @@
+const fs = require('fs');
+const path = require('path');
+const { updateAllSinceTags } = require('./update-since-tags');
+
+/**
+ * Files that contain version numbers
+ */
+const VERSION_FILES = {
+    php: 'wp-graphql.php',
+    constants: 'constants.php',
+    package: 'package.json',
+    readme: 'readme.txt'
+};
+
+/**
+ * Default version to use when resetting
+ */
+const DEFAULT_VERSION = '2.1.0';
+
+/**
+ * Validate version string format
+ */
+function isValidVersion(version) {
+    // Match standard version (2.1.0) or beta version (2.1.0-beta.1)
+    return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(version);
+}
+
+/**
+ * Get current versions from all files
+ */
+function getCurrentVersions(skipReset = false) {
+    const versions = {
+        php: null,
+        constants: null,
+        package: null,
+        readme: null
+    };
+
+    try {
+        // Get version from wp-graphql.php
+        if (fs.existsSync(VERSION_FILES.php)) {
+            const phpContent = fs.readFileSync(VERSION_FILES.php, 'utf8');
+            const phpMatch = phpContent.match(/Version:\s*([0-9.]+(-[a-zA-Z0-9.]+)?)/);
+            versions.php = phpMatch ? phpMatch[1] : null;
+        }
+
+        // Get version from constants.php
+        if (fs.existsSync(VERSION_FILES.constants)) {
+            const constantsContent = fs.readFileSync(VERSION_FILES.constants, 'utf8');
+            const constantsMatch = constantsContent.match(/WPGRAPHQL_VERSION',\s*'([0-9.]+(-[a-zA-Z0-9.]+)?)'/);
+            versions.constants = constantsMatch ? constantsMatch[1] : null;
+        }
+
+        // Get version from package.json
+        if (fs.existsSync(VERSION_FILES.package)) {
+            const packageContent = JSON.parse(fs.readFileSync(VERSION_FILES.package, 'utf8'));
+            versions.package = packageContent.version;
+        }
+
+        // Get stable tag from readme.txt
+        if (fs.existsSync(VERSION_FILES.readme)) {
+            const readmeContent = fs.readFileSync(VERSION_FILES.readme, 'utf8');
+            const readmeMatch = readmeContent.match(/Stable tag:\s*([0-9.]+(-[a-zA-Z0-9.]+)?)/);
+            versions.readme = readmeMatch ? readmeMatch[1] : null;
+        }
+
+        // Check if any versions are invalid or missing
+        Object.keys(versions).forEach(key => {
+            const version = versions[key];
+            if (!version || !isValidVersion(version)) {
+                console.warn(`Warning: Invalid or missing version in ${key}: ${version}`);
+                versions[key] = DEFAULT_VERSION;
+            }
+        });
+
+        return versions;
+    } catch (error) {
+        console.error('Error reading version files:', error);
+        console.error('Current working directory:', process.cwd());
+        console.error('Files checked:', VERSION_FILES);
+
+        // Return default versions on error
+        return {
+            php: DEFAULT_VERSION,
+            constants: DEFAULT_VERSION,
+            package: DEFAULT_VERSION,
+            readme: DEFAULT_VERSION
+        };
+    }
+}
+
+/**
+ * Reset all version numbers to a known good state
+ */
+function resetVersions(version = DEFAULT_VERSION) {
+    if (!isValidVersion(version)) {
+        throw new Error(`Invalid version format: ${version}`);
+    }
+
+    try {
+        // Update wp-graphql.php version
+        if (fs.existsSync(VERSION_FILES.php)) {
+            let content = fs.readFileSync(VERSION_FILES.php, 'utf8');
+            content = content.replace(/(Version:\s*).+/, `$1${version}`);
+            fs.writeFileSync(VERSION_FILES.php, content);
+        } else {
+            console.warn(`${VERSION_FILES.php} not found, skipping`);
+        }
+
+        // Update constants.php version
+        if (fs.existsSync(VERSION_FILES.constants)) {
+            let content = fs.readFileSync(VERSION_FILES.constants, 'utf8');
+            content = content.replace(/(WPGRAPHQL_VERSION',\s*').+(')/, `$1${version}$2`);
+            fs.writeFileSync(VERSION_FILES.constants, content);
+        } else {
+            console.warn(`${VERSION_FILES.constants} not found, skipping`);
+        }
+
+        // Update package.json version
+        if (fs.existsSync(VERSION_FILES.package)) {
+            let packageJson = JSON.parse(fs.readFileSync(VERSION_FILES.package, 'utf8'));
+            packageJson.version = version;
+            fs.writeFileSync(VERSION_FILES.package, JSON.stringify(packageJson, null, 2) + '\n');
+        } else {
+            console.warn(`${VERSION_FILES.package} not found, skipping`);
+        }
+
+        // Update readme.txt stable tag
+        if (fs.existsSync(VERSION_FILES.readme)) {
+            let content = fs.readFileSync(VERSION_FILES.readme, 'utf8');
+            content = content.replace(/(Stable tag:\s*).+/, `$1${version}`);
+            fs.writeFileSync(VERSION_FILES.readme, content);
+        } else {
+            console.warn(`${VERSION_FILES.readme} not found, skipping`);
+        }
+
+        // Return default versions if no files exist
+        return {
+            php: version,
+            constants: version,
+            package: version,
+            readme: version
+        };
+    } catch (error) {
+        throw new Error(`Error resetting versions: ${error.message}`);
+    }
+}
+
+/**
+ * Validate that versions match across files
+ */
+function validateVersions(versions) {
+    const versionSet = new Set(Object.values(versions).filter(Boolean));
+
+    // For beta releases, readme.txt stable tag should not match other versions
+    const isBeta = versions.package && versions.package.includes('-');
+    const versionsToCompare = isBeta ?
+        { php: versions.php, constants: versions.constants, package: versions.package } :
+        versions;
+
+    const nonReadmeVersions = new Set(Object.entries(versionsToCompare)
+        .filter(([key]) => key !== 'readme')
+        .map(([_, value]) => value)
+        .filter(Boolean)
+    );
+
+    if (nonReadmeVersions.size > 1) {
+        throw new Error('Version mismatch across files: ' + JSON.stringify(versions, null, 2));
+    }
+
+    if (isBeta && versions.readme === versions.package) {
+        throw new Error('Stable tag should not be updated for beta releases');
+    }
+
+    return true;
+}
+
+/**
+ * Validate version jump isn't too large
+ */
+function validateVersionJump(currentVersion, newVersion) {
+    const [currentMajor, currentMinor, currentPatch] = currentVersion.split('.').map(Number);
+    const [newMajor, newMinor, newPatch] = newVersion.split('.').map(Number);
+
+    // Don't allow jumping more than one major version
+    if (newMajor > currentMajor + 1) {
+        throw new Error(`Version jump too large: ${currentVersion} → ${newVersion}. Cannot increment major version by more than 1.`);
+    }
+
+    // If same major, don't allow jumping more than one minor version
+    if (newMajor === currentMajor && newMinor > currentMinor + 1) {
+        throw new Error(`Version jump too large: ${currentVersion} → ${newVersion}. Cannot increment minor version by more than 1.`);
+    }
+
+    // If same major and minor, don't allow jumping more than one patch version
+    if (newMajor === currentMajor && newMinor === currentMinor && newPatch > currentPatch + 1) {
+        throw new Error(`Version jump too large: ${currentVersion} → ${newVersion}. Cannot increment patch version by more than 1.`);
+    }
+
+    return true;
+}
+
+/**
+ * Update version numbers across all files
+ */
+async function updateVersions(newVersion, isBeta = false) {
+    try {
+        // Get current versions
+        const currentVersions = getCurrentVersions();
+
+        // Validate version jump
+        validateVersionJump(currentVersions.package, newVersion);
+
+        // Update wp-graphql.php
+        let content = fs.readFileSync(VERSION_FILES.php, 'utf8');
+        content = content.replace(/(Version:\s*).+/, `$1${newVersion}`);
+        fs.writeFileSync(VERSION_FILES.php, content);
+
+        // Update constants.php
+        content = fs.readFileSync(VERSION_FILES.constants, 'utf8');
+        content = content.replace(/(WPGRAPHQL_VERSION',\s*').+(')/, `$1${newVersion}$2`);
+        fs.writeFileSync(VERSION_FILES.constants, content);
+
+        // Update package.json
+        const packageJson = JSON.parse(fs.readFileSync(VERSION_FILES.package, 'utf8'));
+        packageJson.version = newVersion;
+        fs.writeFileSync(VERSION_FILES.package, JSON.stringify(packageJson, null, 2) + '\n');
+
+        // Update readme.txt stable tag (only for non-beta releases)
+        if (!isBeta) {
+            content = fs.readFileSync(VERSION_FILES.readme, 'utf8');
+            content = content.replace(/(Stable tag:\s*).+/, `$1${newVersion}`);
+            fs.writeFileSync(VERSION_FILES.readme, content);
+        }
+
+        // Update @since tags
+        await updateAllSinceTags(newVersion);
+
+        // Validate the updates
+        const versions = getCurrentVersions();
+        validateVersions(versions);
+
+        return true;
+    } catch (error) {
+        throw new Error(`Error updating versions: ${error.message}`);
+    }
+}
+
+module.exports = {
+    getCurrentVersions,
+    validateVersions,
+    updateVersions,
+    resetVersions,
+    isValidVersion
+};
\ No newline at end of file
diff --git a/src/Model/Post.php b/src/Model/Post.php
index 0f3dcef29..e434665e4 100644
--- a/src/Model/Post.php
+++ b/src/Model/Post.php
@@ -819,7 +819,7 @@ protected function init() {
 						_doing_it_wrong(
 							__METHOD__,
 							'`sourceUrlsBySize` is deprecated. Use the `sourceUrlBySize` callable instead.',
-							'@todo'
+							'1.29.1'
 						);
 
 						/**
diff --git a/src/Request.php b/src/Request.php
index 4eb5a1e30..fb9b0021c 100644
--- a/src/Request.php
+++ b/src/Request.php
@@ -730,7 +730,7 @@ private function is_valid_http_content_type(): bool {
 		 * @param bool $is_valid Whether the content type is valid
 		 * @param string $content_type The content type header value that was received
 		 *
-		 * @since todo
+		 * @since next-version
 		 */
 		return (bool) apply_filters( 'graphql_is_valid_http_content_type', $is_valid, $content_type );
 	}
diff --git a/src/Type/Enum/ScriptLoadingGroupLocationEnum.php b/src/Type/Enum/ScriptLoadingGroupLocationEnum.php
index 035d0f089..45876b3c1 100644
--- a/src/Type/Enum/ScriptLoadingGroupLocationEnum.php
+++ b/src/Type/Enum/ScriptLoadingGroupLocationEnum.php
@@ -3,7 +3,7 @@
  * Register the ScriptLoadingGroupLocationEnum Type to the Schema
  *
  * @package WPGraphQL\Type\Enum
- * @since TBD
+ * @since 1.30.0
  */
 
 namespace WPGraphQL\Type\Enum;
diff --git a/wp-graphql.php b/wp-graphql.php
index 8f566f8ba..88020ef32 100644
--- a/wp-graphql.php
+++ b/wp-graphql.php
@@ -6,7 +6,7 @@
  * Description: GraphQL API for WordPress
  * Author: WPGraphQL
  * Author URI: http://www.wpgraphql.com
- * Version: 2.0.0
+ * Version: 2.1.0
  * Text Domain: wp-graphql
  * Domain Path: /languages/
  * Requires at least: 6.0
@@ -18,7 +18,7 @@
  * @package  WPGraphQL
  * @category Core
  * @author   WPGraphQL
- * @version  2.0.0
+ * @version  2.1.0
  */
 
 // Exit if accessed directly.


Test patch:
diff --git a/.github/workflows/graphiql-e2e-tests.yml b/.github/workflows/graphiql-e2e-tests.yml
index 506e95da3..2625f64ff 100644
--- a/.github/workflows/graphiql-e2e-tests.yml
+++ b/.github/workflows/graphiql-e2e-tests.yml
@@ -1,23 +1,22 @@
-name: GraphiQL End-to-End Tests
+name: GraphiQL E2E Tests
 
 on:
   push:
     branches:
       - develop
       - master
-      - release/*
+      - "1.x/master"
+      - next-major
   pull_request:
     branches:
       - develop
       - master
-      - release/*
+      - "1.x/master"
+      - next-major
     paths:
-      - 'package.json'
-      - 'package-lock.json'
       - 'packages/**'
-      - '**.js'
-      - '.github/workflows/*.yml'
-      - '!docs/**'
+      - 'src/Admin/GraphiQL/**'
+      - '.github/workflows/graphiql-e2e-tests.yml'
 
 # Cancel previous workflow run groups that have not completed.
 concurrency:
@@ -27,46 +26,47 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-    e2e-tests:
-        name: E2E tests on Node ${{ matrix.node-version }}
-        runs-on: ubuntu-latest
+  test:
+    name: E2E Tests
+    runs-on: ubuntu-latest
 
-        steps:
-            - name: Checkout code
-              uses: actions/checkout@v4
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
 
-            - name: Setup Node.js
-              uses: actions/setup-node@v4
-              with:
-                  node-version: 20
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+          cache: 'npm'
 
-            - name: Setup PHP
-              uses: shivammathur/setup-php@v2
-              with:
-                php-version: 8.2
-                extensions: mbstring, intl
-                tools: composer
+      - name: Setup PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: 8.2
+          extensions: mbstring, intl
+          tools: composer
 
-            - name: Install dependencies
-              uses: ramsey/composer-install@v3
-              with:
-                composer-options: "--no-progress"
+      - name: Install dependencies
+        uses: ramsey/composer-install@v3
+        with:
+          composer-options: "--no-progress"
 
-            - name: Install JavaScript dependencies
-              run: npm ci
+      - name: Install JavaScript dependencies
+        run: npm ci
 
-            - name: Build Assets
-              run: npm run build
+      - name: Build Assets
+        run: npm run build
 
-            - name: Install Playwright dependencies
-              run: |
-                  npx playwright install chromium firefox webkit --with-deps
+      - name: Install Playwright dependencies
+        run: |
+            npx playwright install chromium firefox webkit --with-deps
 
-            - name: Start WordPress Environment
-              run: npm run wp-env -- start
+      - name: Start WordPress Environment
+        run: npm run wp-env -- start
 
-            - name: Run E2E tests
-              run: npm run test:e2e
+      - name: Run E2E tests
+        run: npm run test:e2e
 
-            - name: Stop WordPress Environment
-              run: npm run wp-env -- stop
+      - name: Stop WordPress Environment
+        run: npm run wp-env -- stop
diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml
new file mode 100644
index 000000000..37f7954ab
--- /dev/null
+++ b/.github/workflows/test-scripts.yml
@@ -0,0 +1,44 @@
+name: Test Scripts
+
+on:
+  push:
+    branches:
+      - develop
+      - master
+      - "1.x/master"
+      - next-major
+  pull_request:
+    branches:
+      - develop
+      - master
+      - "1.x/master"
+      - next-major
+    paths:
+      - 'scripts/**'
+      - 'package.json'
+      - 'package-lock.json'
+      - '.github/workflows/test-scripts.yml'
+
+jobs:
+  test:
+    name: Node ${{ matrix.node }} Tests
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node: [20, 21]
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Setup Node.js ${{ matrix.node }}
+        uses: actions/setup-node@v4
+        with:
+          node-version: ${{ matrix.node }}
+          cache: 'npm'
+
+      - name: Install Dependencies
+        run: npm ci
+
+      - name: Run Tests
+        run: npm run test:changesets
\ No newline at end of file
diff --git a/docs/testing.md b/docs/testing.md
index 662ae524e..d509f94cc 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -5,7 +5,7 @@ title: "Testing"
 
 This document will be most useful for developers that want to contribute to WPGraphQL and want to run tests locally.
 
-In order to run tests, you must [clone the plugin from Github](https://github.com/wp-graphql/wp-graphql). Downloading from Composer or Packagist will not include the dev dependencies needed to run tests.
+In order to run tests, you must [clone the plugin from GitHub](https://github.com/wp-graphql/wp-graphql). Downloading from Composer or Packagist will not include the dev dependencies needed to run tests.
 
 ## Testing Locally with Codeception
 
@@ -22,11 +22,11 @@ Running tests locally with Codeception is the fastest way to run tests, but it r
 
 WPGraphQL includes a script to install a local test environment.
 
-Running this script will install WordPress to your machines `tmp` directory, and will add WPGraphQL as a plugin and activate it.
+Running this script will install WordPress to your machine's `tmp` directory, and will add WPGraphQL as a plugin and activate it.
 
 > **NOTE:** Because this installs to a tmp directory, if you restart your computer, this will go away so you would need to run this script again to setup your test environment again.
 
-Run the following command, replacing the variables with data for your local Database
+Run the following command, replacing the variables with data for your local Database:
 
 ```shell
 bin/install-test-env.sh $db_name $db_user $db_pass $db_host latest true
@@ -142,7 +142,7 @@ To run the tests, run the following commands (you can use `control + c` to exit)
 vendor/bin/codecept run wpunit
 ```
 
-This will run *all* of the tests of the `wpunit` suite.
+This will run _all_ of the tests of the `wpunit` suite.
 
 To run an individual test file, you can specify the file like so:
 
@@ -162,7 +162,7 @@ The tests should start running and you should see something similar to the follo
 
 ## Testing with Docker
 
-Testing in docker is slower than testing locally directly with Codeception, but it allows you to test in a consistent environment and not have to worry about a local environment issue getting in the way of running the tests. Also allows testing with different versions of PHP and/or WordPress quickly.
+Testing in Docker is slower than testing locally directly with Codeception, but it allows you to test in a consistent environment and not have to worry about a local environment issue getting in the way of running the tests. It also allows testing with different versions of PHP and/or WordPress quickly.
 
 ### Pre-Requisites
 
@@ -176,7 +176,7 @@ Build and start the Docker testing environment by running this command:
 composer build-test
 ```
 
-This step will take several minutes the first time it's run because it needs to install OS dependencies. This work will be cached so you won't have to wait as long the next time you run it. You are ready to go to the next step to run the full test suite in the docker container.
+This step will take several minutes the first time it's run because it needs to install OS dependencies. This work will be cached so you won't have to wait as long the next time you run it. You are ready to go to the next step to run the full test suite in the Docker container.
 
 Build the environment with specific version of PHP:
 
@@ -214,7 +214,7 @@ composer run-test
 
 Use the environment variable SUITES to specify individual files or lists of files to test. This is useful when working on one test file and wanting to limit test execution to the single file or test. Also see the contributing documentation on enabling xdebug in the testing docker environment.
 
-By default the SUITES variable is set to all test types; acceptance,functional,wpunit.  See the .env file in the source root directory.
+By default the SUITES variable is set to all test types; acceptance,functional,wpunit. See the .env file in the source root directory.
 
 To run just the acceptance test suite, use the following:
 
@@ -260,13 +260,13 @@ PHP_VERSION=8.1 WP_VERSION=6.0 SUITES=acceptance composer run-test
 
 **Notes:**
 
--   If you make a change that requires `composer install` to be rerun, run composer build-app again.
+- If you make a change that requires `composer install` to be rerun, run composer build-app again.
 
--   Leave the container shell by typing `exit`.
+- Leave the container shell by typing `exit`.
 
--   Docker artifacts will *usually* be cleaned up automatically when the script completes. In case it doesn't do the job, try these solutions:
-    - Run this command: `docker system prune`
-    - https://docs.docker.com/config/pruning/#prune-containers
+- Docker artifacts will _usually_ be cleaned up automatically when the script completes. In case it doesn't do the job, try these solutions:
+  - Run this command: `docker system prune`
+  - https://docs.docker.com/config/pruning/#prune-containers
 
 #### Advanced Testing Within Docker Shell
 
@@ -282,7 +282,7 @@ Specify the PHP and/or WordPress versions to use that environment. Environment m
 PHP_VERSION=8.1 WP_VERSION=6.0 docker compose run --entrypoint bash -- testing
 ```
 
-Run the setup script, which also runs the test suite.  This needs to be run at least once after logging into the docker bash shell prompt. If you log out, the settings are not saved and must be re-run after opening the docker shell prompt.
+Run the setup script, which also runs the test suite. This needs to be run at least once after logging into the docker bash shell prompt. If you log out, the settings are not saved and must be re-run after opening the docker shell prompt.
 
 ```shell
 /usr/local/bin/testing-entrypoint.sh
diff --git a/scripts/__tests__/changelog-formatters.test.js b/scripts/__tests__/changelog-formatters.test.js
new file mode 100644
index 000000000..507244db2
--- /dev/null
+++ b/scripts/__tests__/changelog-formatters.test.js
@@ -0,0 +1,106 @@
+const { formatChangelogMd } = require('../changelog-formatters/changelog-md');
+const { formatReadmeTxt } = require('../changelog-formatters/readme-txt');
+
+describe('Changelog Formatters', () => {
+    const mockChangeset = {
+        summary: 'feat: Add new GraphQL field',
+        type: 'minor',
+        pr: 123,
+        pr_number: '123',
+        pr_url: 'https://github.com/wp-graphql/wp-graphql/pull/123',
+        breaking: false,
+        breaking_changes: '',
+        upgrade_instructions: '',
+        releases: [{ name: 'wp-graphql', type: 'minor' }]
+    };
+
+    const mockBreakingChangeset = {
+        summary: 'feat!: Breaking change',
+        type: 'major',
+        pr: 124,
+        pr_number: '124',
+        pr_url: 'https://github.com/wp-graphql/wp-graphql/pull/124',
+        breaking: true,
+        breaking_changes: 'This breaks the existing API',
+        upgrade_instructions: 'Update your queries to use the new format',
+        releases: [{ name: 'wp-graphql', type: 'major' }]
+    };
+
+    describe('CHANGELOG.md Formatter', () => {
+        test('formats basic feature change', () => {
+            const result = formatChangelogMd('2.1.0', [mockChangeset]);
+
+            expect(result).toContain('## [2.1.0]');
+            expect(result).toContain('### Features');
+            expect(result).toContain('- Add new GraphQL field ([#123](https://github.com/wp-graphql/wp-graphql/pull/123))');
+        });
+
+        test('formats breaking change', () => {
+            const result = formatChangelogMd('3.0.0', [mockBreakingChangeset]);
+
+            expect(result).toContain('## [3.0.0]');
+            expect(result).toContain('### ⚠ BREAKING CHANGES');
+            expect(result).toContain('This breaks the existing API');
+            expect(result).toContain('#### Upgrade Instructions');
+            expect(result).toContain('Update your queries to use the new format');
+            expect(result).toContain('([#124](https://github.com/wp-graphql/wp-graphql/pull/124))');
+        });
+
+        test('groups multiple changes by type', () => {
+            const changes = [
+                mockChangeset,
+                { ...mockChangeset, summary: 'fix: Fix bug', type: 'patch', pr: 125 },
+                { ...mockChangeset, summary: 'docs: Update docs', type: 'patch', pr: 126 }
+            ];
+
+            const result = formatChangelogMd('2.1.0', changes);
+
+            expect(result).toContain('### Features');
+            expect(result).toContain('### Bug Fixes');
+            expect(result).toContain('### Documentation');
+        });
+    });
+
+    describe('readme.txt Formatter', () => {
+        test('formats basic feature change', () => {
+            const result = formatReadmeTxt('2.1.0', [mockChangeset]);
+
+            expect(result).toContain('= 2.1.0 =');
+            expect(result).toContain('**New Features**');
+            expect(result).toContain('* Add new GraphQL field');
+        });
+
+        test('formats breaking change', () => {
+            const result = formatReadmeTxt('3.0.0', [mockBreakingChangeset]);
+
+            expect(result).toContain('= 3.0.0 =');
+            expect(result).toContain('**BREAKING CHANGES**');
+            expect(result).toContain('This breaks the existing API');
+            expect(result).toContain('Upgrade Instructions:');
+            expect(result).toContain('Update your queries to use the new format');
+        });
+
+        test('adds upgrade notice for breaking changes', () => {
+            const result = formatReadmeTxt('3.0.0', [mockBreakingChangeset]);
+
+            expect(result).toContain('== Upgrade Notice ==');
+            expect(result).toContain('= 3.0.0 =');
+            expect(result).toContain('**BREAKING CHANGE UPDATE**');
+            expect(result).toContain('This breaks the existing API');
+        });
+
+        test('groups multiple changes by type', () => {
+            const changes = [
+                mockChangeset,
+                { ...mockChangeset, summary: 'fix: Fix bug', type: 'patch', pr: 125 },
+                { ...mockChangeset, summary: 'docs: Update docs', type: 'patch', pr: 126 }
+            ];
+
+            const result = formatReadmeTxt('2.1.0', changes);
+
+            expect(result).toContain('**New Features**');
+            expect(result).toContain('**Bug Fixes**');
+            expect(result).not.toContain('**Documentation**'); // Docs changes not shown in readme.txt
+        });
+    });
+});
\ No newline at end of file
diff --git a/scripts/__tests__/changelog-formatters/readme-txt.test.js b/scripts/__tests__/changelog-formatters/readme-txt.test.js
new file mode 100644
index 000000000..dedb5de4a
--- /dev/null
+++ b/scripts/__tests__/changelog-formatters/readme-txt.test.js
@@ -0,0 +1,126 @@
+const { getReadmeTxtChangelog, getUpgradeNoticeEntry, updateReadmeTxt } = require('../../changelog-formatters/readme-txt');
+const defaultChangelogFunctions = require('@changesets/cli/changelog');
+const fs = require('fs');
+const path = require('path');
+
+jest.mock('fs');
+jest.mock('@changesets/get-github-info');
+
+describe('Changelog Integration', () => {
+    beforeEach(() => {
+        jest.clearAllMocks();
+        fs.readFileSync.mockReturnValue('== Changelog ==\n\nOld entries\n\n== Upgrade Notice ==\n\nOld notices');
+    });
+
+    describe('Upgrade Notice Generation', () => {
+        test('generates upgrade notice for breaking changes', async () => {
+            const release = {
+                newVersion: '2.0.0',
+                changesets: [{
+                    breaking: true,
+                    breaking_changes: 'This is a breaking change',
+                    pr_number: '123',
+                    upgrade_instructions: 'Follow these steps'
+                }]
+            };
+
+            const notice = await getUpgradeNoticeEntry(release, {
+                repo: 'wp-graphql/wp-graphql'
+            });
+
+            expect(notice).toContain('= 2.0.0 =');
+            expect(notice).toContain('**BREAKING CHANGE UPDATE**');
+            expect(notice).toContain('This is a breaking change');
+            expect(notice).toContain('In <a href="wp-graphql/wp-graphql/pull/123">#123</a>');
+            expect(notice).toContain('Follow these steps');
+        });
+
+        test('generates standard notice for minor versions', async () => {
+            const release = {
+                newVersion: '2.1.0',
+                changesets: []
+            };
+
+            const notice = await getUpgradeNoticeEntry(release, {});
+            expect(notice).toContain('= 2.1.0 =');
+            expect(notice).toContain('While there are no known breaking changes');
+            expect(notice).toContain('recommend testing on staging servers');
+        });
+
+        test('skips notice for patch versions without breaking changes', async () => {
+            const release = {
+                newVersion: '2.0.1',
+                changesets: []
+            };
+
+            const notice = await getUpgradeNoticeEntry(release, {});
+            expect(notice).toBe('');
+        });
+    });
+
+    describe('Readme.txt Integration', () => {
+        test('updates both changelog and upgrade notice sections', async () => {
+            const release = {
+                newVersion: '2.0.0',
+                changesets: [{
+                    breaking: true,
+                    breakingChanges: 'Breaking change',
+                    upgradeInstructions: 'Upgrade steps',
+                    summary: 'New feature',
+                    pr: 123
+                }]
+            };
+
+            const options = { repo: 'wp-graphql/wp-graphql' };
+            await updateReadmeTxt(release, options);
+
+            expect(fs.writeFileSync).toHaveBeenCalledWith(
+                expect.any(String),
+                expect.stringContaining('== Changelog ==')
+            );
+            expect(fs.writeFileSync).toHaveBeenCalledWith(
+                expect.any(String),
+                expect.stringContaining('== Upgrade Notice ==')
+            );
+            expect(fs.writeFileSync).toHaveBeenCalledWith(
+                expect.any(String),
+                expect.stringContaining('**BREAKING CHANGE UPDATE**')
+            );
+        });
+
+        test('handles missing upgrade notice section', async () => {
+            fs.readFileSync.mockReturnValue('== Changelog ==\n\nOld entries');
+
+            const release = {
+                newVersion: '2.0.0',
+                changesets: [{
+                    breaking: true,
+                    breakingChanges: 'Breaking change',
+                    upgradeInstructions: 'Upgrade steps'
+                }]
+            };
+
+            await updateReadmeTxt(release, {});
+
+            expect(fs.writeFileSync).toHaveBeenCalledWith(
+                expect.any(String),
+                expect.stringContaining('== Upgrade Notice ==\n\n= 2.0.0 =')
+            );
+        });
+
+        test('preserves stable tag for beta releases', async () => {
+            const release = {
+                newVersion: '2.0.0-beta.1',
+                changesets: [{
+                    breaking: true,
+                    breakingChanges: 'Beta breaking change'
+                }]
+            };
+
+            await updateReadmeTxt(release, {});
+
+            const writeCall = fs.writeFileSync.mock.calls[0][1];
+            expect(writeCall).not.toMatch(/Stable tag: 2.0.0-beta.1/);
+        });
+    });
+});
\ No newline at end of file
diff --git a/scripts/__tests__/changelog-integration.test.js b/scripts/__tests__/changelog-integration.test.js
new file mode 100644
index 000000000..d4cde5377
--- /dev/null
+++ b/scripts/__tests__/changelog-integration.test.js
@@ -0,0 +1,83 @@
+const { getReadmeTxtChangelog } = require('../changelog-formatters/readme-txt');
+const defaultChangelogFunctions = require('@changesets/cli/changelog');
+const fs = require('fs');
+
+jest.mock('fs');
+jest.mock('@changesets/get-github-info');
+
+jest.mock('@changesets/cli/changelog', () => ({
+    getReleaseLine: async (changeset) => `${changeset.summary} (${changeset.commit})`,
+    default: {
+        getReleaseLine: async (changeset) => `${changeset.summary} (${changeset.commit})`
+    }
+}));
+
+describe('Changelog Integration', () => {
+    beforeEach(() => {
+        jest.clearAllMocks();
+        // Mock both files existing
+        fs.readFileSync
+            .mockReturnValueOnce('Stable tag: 1.0.0') // readme.txt
+            .mockReturnValueOnce('# Changelog'); // CHANGELOG.md
+    });
+
+    test('both formatters generate appropriate changelog entries', async () => {
+        const release = {
+            newVersion: '2.0.0',
+            changesets: [
+                {
+                    summary: 'feat: new feature',
+                    commit: 'abc123',
+                    id: 'unique-id'
+                }
+            ]
+        };
+
+        const options = { repo: 'wp-graphql/wp-graphql' };
+
+        // Get changelog entries from both formatters
+        const readmeTxtChangelog = await getReadmeTxtChangelog(release, options);
+        const defaultChangelog = await defaultChangelogFunctions.getReleaseLine(
+            release.changesets[0],
+            options
+        );
+
+        // Verify readme.txt format
+        expect(readmeTxtChangelog).toContain('= 2.0.0 =');
+        expect(readmeTxtChangelog).toContain('**New Features**');
+        expect(readmeTxtChangelog).toContain('* feat: new feature');
+
+        // Verify CHANGELOG.md format
+        expect(defaultChangelog).toContain('new feature');
+        expect(defaultChangelog).toContain('abc123');
+    });
+
+    test('handles beta releases appropriately in both formats', async () => {
+        const release = {
+            newVersion: '2.0.0-beta.1',
+            changesets: [
+                {
+                    summary: 'feat: beta feature',
+                    commit: 'def456',
+                    id: 'beta-id'
+                }
+            ]
+        };
+
+        const options = { repo: 'wp-graphql/wp-graphql' };
+
+        const readmeTxtChangelog = await getReadmeTxtChangelog(release, options);
+        const defaultChangelog = await defaultChangelogFunctions.getReleaseLine(
+            release.changesets[0],
+            options
+        );
+
+        // Verify readme.txt format for beta
+        expect(readmeTxtChangelog).toContain('= 2.0.0-beta.1 =');
+        expect(fs.writeFileSync).not.toHaveBeenCalled(); // Stable tag not updated
+
+        // Verify CHANGELOG.md format includes beta version
+        expect(defaultChangelog).toContain('beta feature');
+        expect(defaultChangelog).toContain('def456');
+    });
+});
\ No newline at end of file
diff --git a/scripts/__tests__/generate-changeset.test.js b/scripts/__tests__/generate-changeset.test.js
new file mode 100644
index 000000000..c62808888
--- /dev/null
+++ b/scripts/__tests__/generate-changeset.test.js
@@ -0,0 +1,365 @@
+const { parseTitle, parsePRBody, createChangeset, formatSummary, ALLOWED_TYPES } = require('../generate-changeset');
+const { generateSinceTagsMetadata } = require('../scan-since-tags');
+const fs = require('fs');
+const path = require('path');
+
+jest.mock('../scan-since-tags', () => ({
+    generateSinceTagsMetadata: jest.fn()
+}));
+
+jest.mock('fs', () => ({
+    existsSync: jest.fn(),
+    writeFileSync: jest.fn(),
+    mkdirSync: jest.fn()
+}));
+
+describe('Changeset Generation', () => {
+    beforeEach(() => {
+        jest.clearAllMocks();
+        generateSinceTagsMetadata.mockResolvedValue({
+            sinceFiles: ['test.php'],
+            totalTags: 1
+        });
+        fs.existsSync.mockReturnValue(true);
+        fs.writeFileSync.mockImplementation(() => {});
+        fs.mkdirSync.mockImplementation(() => {});
+    });
+
+    describe('parseTitle', () => {
+        test('parses basic title format', () => {
+            const result = parseTitle('feat: Add new feature');
+            expect(result).toEqual({
+                type: 'feat',
+                isBreaking: false
+            });
+        });
+
+        test('parses title with scope', () => {
+            const result = parseTitle('feat(core): Add new feature');
+            expect(result).toEqual({
+                type: 'feat',
+                isBreaking: false
+            });
+        });
+
+        test('detects breaking change with !', () => {
+            const result = parseTitle('feat!: Breaking feature');
+            expect(result).toEqual({
+                type: 'feat',
+                isBreaking: true
+            });
+        });
+
+        test('detects breaking change with BREAKING CHANGE', () => {
+            const result = parseTitle('feat: BREAKING CHANGE - New feature');
+            expect(result).toEqual({
+                type: 'feat',
+                isBreaking: true
+            });
+        });
+
+        test('throws on invalid type', () => {
+            expect(() => parseTitle('invalid: Some change')).toThrow('PR title does not follow conventional commit format');
+        });
+
+        test('validates allowed types', () => {
+            ALLOWED_TYPES.forEach(type => {
+                const result = parseTitle(`${type}: Some change`);
+                expect(result.type).toBe(type);
+            });
+        });
+    });
+
+    describe('parsePRBody', () => {
+        test('extracts all sections with ### headings', () => {
+            const body = `
+                What does this implement/fix? Explain your changes.
+                ---
+                This is a description
+
+                ### Breaking Changes
+                This breaks something
+
+                ### Upgrade Instructions
+                Follow these steps
+            `;
+
+            const result = parsePRBody(body);
+            expect(result).toEqual({
+                description: 'This is a description',
+                breaking: 'This breaks something',
+                upgrade: 'Follow these steps'
+            });
+        });
+
+        test('extracts all sections with ## headings', () => {
+            const body = `
+                What does this implement/fix? Explain your changes.
+                ---
+                This is a description
+
+                ## Breaking Changes
+                This breaks something
+
+                ## Upgrade Instructions
+                Follow these steps
+            `;
+
+            const result = parsePRBody(body);
+            expect(result).toEqual({
+                description: 'This is a description',
+                breaking: 'This breaks something',
+                upgrade: 'Follow these steps'
+            });
+        });
+
+        test('handles mixed heading levels', () => {
+            const body = `
+                What does this implement/fix? Explain your changes.
+                ---
+                This is a description
+
+                ## Breaking Changes
+                This breaks something
+
+                ### Upgrade Instructions
+                Follow these steps
+            `;
+
+            const result = parsePRBody(body);
+            expect(result).toEqual({
+                description: 'This is a description',
+                breaking: 'This breaks something',
+                upgrade: 'Follow these steps'
+            });
+        });
+
+        test('handles missing sections', () => {
+            const body = 'What does this implement/fix? Explain your changes.\n---\nJust a description';
+            const result = parsePRBody(body);
+            expect(result).toEqual({
+                description: 'Just a description',
+                breaking: '',
+                upgrade: ''
+            });
+        });
+
+        test('cleans up N/A placeholders', () => {
+            const body = `
+                What does this implement/fix? Explain your changes.
+                ---
+                Description
+
+                ## Breaking Changes
+                N/A
+
+                ## Upgrade Instructions
+                none
+            `;
+
+            const result = parsePRBody(body);
+            expect(result.breaking).toBe('');
+            expect(result.upgrade).toBe('');
+        });
+    });
+
+    describe('createChangeset', () => {
+        const validPR = {
+            title: 'feat: New feature',
+            body: `
+                What does this implement/fix? Explain your changes.
+                ---
+                Adds a new feature
+
+                ### Breaking Changes
+
+                ### Upgrade Instructions
+            `,
+            prNumber: '123'
+        };
+
+        test('creates basic changeset', async () => {
+            const result = await createChangeset(validPR);
+            expect(result).toEqual({
+                type: 'minor',
+                breaking: false,
+                pr: 123,
+                sinceFiles: ['test.php'],
+                totalSinceTags: 1,
+                changesetId: expect.stringContaining('pr-123-')
+            });
+            expect(fs.writeFileSync).toHaveBeenCalled();
+        });
+
+        test('handles breaking changes', async () => {
+            const breakingPR = {
+                title: 'feat!: Breaking feature',
+                body: `
+                    What does this implement/fix? Explain your changes.
+                    ---
+                    Breaking feature description
+
+                    ### Breaking Changes
+                    This breaks something
+
+                    ### Upgrade Instructions
+                    Follow these steps
+                `,
+                prNumber: '123'
+            };
+
+            const result = await createChangeset(breakingPR);
+            expect(result.type).toBe('major');
+            expect(result.breaking).toBe(true);
+
+            // Verify changeset content
+            const writeCall = fs.writeFileSync.mock.calls[0][1];
+            expect(writeCall).toContain('BREAKING CHANGES:');
+            expect(writeCall).toContain('UPGRADE INSTRUCTIONS:');
+        });
+
+        test('requires upgrade instructions for breaking changes', async () => {
+            const breakingPR = {
+                title: 'feat!: Breaking feature',
+                body: `
+                    What does this implement/fix? Explain your changes.
+                    ---
+                    Breaking feature
+
+                    ### Breaking Changes
+                    This breaks something
+                `,
+                prNumber: '123'
+            };
+
+            await expect(createChangeset(breakingPR)).rejects.toThrow('Breaking changes must include upgrade instructions');
+        });
+
+        test('includes @since tags metadata', async () => {
+            const result = await createChangeset(validPR);
+            expect(result.sinceFiles).toEqual(['test.php']);
+            expect(result.totalSinceTags).toBe(1);
+
+            const writeCall = fs.writeFileSync.mock.calls[0][1];
+            expect(writeCall).toContain('FILES WITH @since TAGS TO UPDATE:');
+            expect(writeCall).toContain('test.php');
+        });
+
+        test('creates correct changeset file structure', async () => {
+            await createChangeset(validPR);
+            const writeCall = fs.writeFileSync.mock.calls[0][1];
+
+            // Verify YAML front matter
+            expect(writeCall).toMatch(/^---\n/);
+            expect(writeCall).toMatch(/\n---\n/);
+
+            // Verify JSON content
+            const jsonContent = writeCall.match(/---\n([\s\S]*?)\n---/)[1];
+            const parsed = JSON.parse(jsonContent);
+            expect(parsed).toMatchObject({
+                summary: expect.any(String),
+                type: 'minor',
+                pr: 123,
+                pr_number: '123',
+                pr_url: expect.stringContaining('/123'),
+                breaking: false,
+                releases: [{ name: 'wp-graphql', type: 'minor' }]
+            });
+        });
+
+        test('creates changeset with breaking change from title', async () => {
+            const pr = {
+                title: 'fix!: Breaking change in bug fix',
+                body: `What does this implement/fix? Explain your changes.
+---
+This is a breaking bug fix
+
+## Breaking Changes
+This breaks something
+
+## Upgrade Instructions
+Follow these steps`,
+                prNumber: '123'
+            };
+
+            const result = await createChangeset(pr);
+            expect(result.type).toBe('major');
+            expect(result.breaking).toBe(true);
+
+            // Verify changeset content
+            const writeCall = fs.writeFileSync.mock.calls[0][1];
+            const jsonContent = writeCall.match(/---\n([\s\S]*?)\n---/)[1];
+            const parsed = JSON.parse(jsonContent);
+            expect(parsed.releases[0].type).toBe('major');
+        });
+
+        test('creates changeset with breaking change from PR body', async () => {
+            const pr = {
+                title: 'fix: Non-breaking title',
+                body: `What does this implement/fix? Explain your changes.
+---
+This is a bug fix
+
+## Breaking Changes
+This breaks something
+
+## Upgrade Instructions
+Follow these steps`,
+                prNumber: '123'
+            };
+
+            const result = await createChangeset(pr);
+            expect(result.type).toBe('major');
+            expect(result.breaking).toBe(false); // Title doesn't have breaking change marker
+
+            // Verify changeset content
+            const writeCall = fs.writeFileSync.mock.calls[0][1];
+            const jsonContent = writeCall.match(/---\n([\s\S]*?)\n---/)[1];
+            const parsed = JSON.parse(jsonContent);
+            expect(parsed.releases[0].type).toBe('major');
+            expect(parsed.breaking_changes).toBe('This breaks something');
+        });
+
+        test('creates changeset with breaking change from both title and body', async () => {
+            const pr = {
+                title: 'feat!: Breaking feature change',
+                body: `What does this implement/fix? Explain your changes.
+---
+This is a feature
+
+## Breaking Changes
+This also breaks something
+
+## Upgrade Instructions
+Follow these steps`,
+                prNumber: '123'
+            };
+
+            const result = await createChangeset(pr);
+            expect(result.type).toBe('major');
+            expect(result.breaking).toBe(true);
+
+            // Verify changeset content
+            const writeCall = fs.writeFileSync.mock.calls[0][1];
+            const jsonContent = writeCall.match(/---\n([\s\S]*?)\n---/)[1];
+            const parsed = JSON.parse(jsonContent);
+            expect(parsed.releases[0].type).toBe('major');
+            expect(parsed.breaking_changes).toBe('This also breaks something');
+            expect(parsed.upgrade_instructions).toBe('Follow these steps');
+        });
+    });
+
+    describe('formatSummary', () => {
+        test('formats regular summary', () => {
+            expect(formatSummary('feat', false, 'New feature')).toBe('feat: New feature');
+        });
+
+        test('formats breaking change summary', () => {
+            expect(formatSummary('feat', true, 'Breaking feature')).toBe('feat!: Breaking feature');
+        });
+
+        test('trims description', () => {
+            expect(formatSummary('fix', false, '  Fix bug  ')).toBe('fix: Fix bug');
+        });
+    });
+});
\ No newline at end of file
diff --git a/scripts/__tests__/scan-since-tags.test.js b/scripts/__tests__/scan-since-tags.test.js
new file mode 100644
index 000000000..512cbbc40
--- /dev/null
+++ b/scripts/__tests__/scan-since-tags.test.js
@@ -0,0 +1,70 @@
+const { scanFileForSinceTags, findFilesWithSinceTags, generateSinceTagsMetadata } = require('../scan-since-tags');
+const fs = require('fs');
+const { glob } = require('glob');
+
+jest.mock('fs');
+jest.mock('glob');
+
+describe('Since Tags Scanner', () => {
+    beforeEach(() => {
+        jest.clearAllMocks();
+    });
+
+    describe('scanFileForSinceTags', () => {
+        test('counts @since todo tags correctly', () => {
+            fs.readFileSync.mockReturnValue(`
+                /**
+                 * @since todo
+                 */
+                function test1() {}
+
+                /**
+                 * @since todo
+                 */
+                function test2() {}
+            `);
+
+            expect(scanFileForSinceTags('test.php')).toBe(2);
+        });
+
+        test('handles files without tags', () => {
+            fs.readFileSync.mockReturnValue(`
+                /**
+                 * @since 1.0.0
+                 */
+                function test() {}
+            `);
+
+            expect(scanFileForSinceTags('test.php')).toBe(0);
+        });
+    });
+
+    describe('findFilesWithSinceTags', () => {
+        test('finds all files with tags', async () => {
+            glob.mockResolvedValue(['file1.php', 'file2.php']);
+            fs.readFileSync
+                .mockReturnValueOnce('@since todo')
+                .mockReturnValueOnce('@since todo\n@since todo');
+
+            const results = await findFilesWithSinceTags();
+            expect(results).toHaveLength(2);
+            expect(results[0]).toEqual({ file: 'file1.php', count: 1 });
+            expect(results[1]).toEqual({ file: 'file2.php', count: 2 });
+        });
+    });
+
+    describe('generateSinceTagsMetadata', () => {
+        test('generates correct metadata', async () => {
+            glob.mockResolvedValue(['file1.php', 'file2.php']);
+            fs.readFileSync
+                .mockReturnValueOnce('@since todo')
+                .mockReturnValueOnce('@since todo\n@since todo');
+
+            const metadata = await generateSinceTagsMetadata();
+            expect(metadata).toEqual({
+                sinceFiles: ['file1.php', 'file2.php'],
+                totalTags: 3
+            });
+        });
+    });
+});
\ No newline at end of file
diff --git a/scripts/__tests__/simulate-release.test.js b/scripts/__tests__/simulate-release.test.js
new file mode 100644
index 000000000..29757d427
--- /dev/null
+++ b/scripts/__tests__/simulate-release.test.js
@@ -0,0 +1,47 @@
+const simulateRelease = require('../simulate-release');
+const { getCurrentVersions, updateVersions } = require('../version-management');
+const { updateAllSinceTags } = require('../update-since-tags');
+const { getReadmeTxtChangelog } = require('../changelog-formatters/readme-txt');
+
+jest.mock('../version-management');
+jest.mock('../update-since-tags');
+jest.mock('../changelog-formatters/readme-txt');
+jest.mock('chalk', () => ({
+    blue: (text) => text,
+    green: (text) => text,
+    yellow: (text) => text,
+    red: (text) => text
+}));
+
+describe('Release Simulation', () => {
+    beforeEach(() => {
+        jest.clearAllMocks();
+
+        // Mock successful responses
+        getCurrentVersions.mockReturnValue({
+            php: '1.0.0',
+            constants: '1.0.0',
+            package: '1.0.0',
+            readme: '1.0.0'
+        });
+
+        updateVersions.mockResolvedValue(true);
+        updateAllSinceTags.mockResolvedValue({ updated: ['file1.php'], errors: [] });
+        getReadmeTxtChangelog.mockResolvedValue('Changelog content');
+    });
+
+    test('simulates standard release successfully', async () => {
+        await expect(simulateRelease('2.0.0')).resolves.toBe(true);
+        expect(updateVersions).toHaveBeenCalledWith('2.0.0', false);
+    });
+
+    test('simulates beta release successfully', async () => {
+        await expect(simulateRelease('2.0.0-beta.1', { beta: true })).resolves.toBe(true);
+        expect(updateVersions).toHaveBeenCalledWith('2.0.0-beta.1', true);
+    });
+
+    test('handles errors appropriately', async () => {
+        updateVersions.mockRejectedValue(new Error('Update failed'));
+        await expect(simulateRelease('2.0.0')).rejects.toThrow('Update failed');
+    });
+});
\ No newline at end of file
diff --git a/scripts/__tests__/update-since-tags.test.js b/scripts/__tests__/update-since-tags.test.js
new file mode 100644
index 000000000..1e11256e9
--- /dev/null
+++ b/scripts/__tests__/update-since-tags.test.js
@@ -0,0 +1,137 @@
+const { getSinceTodoTags, updateSinceTags } = require('../update-since-tags');
+const fs = require('fs');
+const path = require('path');
+
+jest.mock('fs');
+
+describe('Since Tags Management', () => {
+    beforeEach(() => {
+        jest.clearAllMocks();
+    });
+
+    describe('getSinceTodoTags', () => {
+        test('counts @since todo tags correctly', () => {
+            const content = `
+                /**
+                 * @since todo
+                 */
+                function test1() {}
+
+                /**
+                 * @since todo
+                 */
+                function test2() {}
+            `;
+
+            expect(getSinceTodoTags(content)).toBe(2);
+        });
+
+        test('handles no tags', () => {
+            const content = `
+                /**
+                 * @since 1.0.0
+                 */
+                function test() {}
+            `;
+
+            expect(getSinceTodoTags(content)).toBe(0);
+        });
+    });
+
+    describe('updateSinceTags', () => {
+        const mockFilePath = 'test.php';
+
+        beforeEach(() => {
+            jest.spyOn(fs, 'readFileSync');
+            jest.spyOn(fs, 'writeFileSync');
+        });
+
+        test('updates @since todo tags with version', () => {
+            const content = `
+                /**
+                 * @since todo
+                 */
+                function test() {}
+            `;
+
+            fs.readFileSync.mockReturnValue(content);
+
+            updateSinceTags(mockFilePath, '2.0.0');
+
+            expect(fs.writeFileSync).toHaveBeenCalledWith(
+                mockFilePath,
+                expect.stringContaining('@since 2.0.0')
+            );
+        });
+
+        test('handles multiple tags in one file', () => {
+            const content = `
+                /**
+                 * @since todo
+                 */
+                function test1() {}
+
+                /**
+                 * @since todo
+                 */
+                function test2() {}
+            `;
+
+            fs.readFileSync.mockReturnValue(content);
+
+            updateSinceTags(mockFilePath, '2.0.0');
+
+            const writtenContent = fs.writeFileSync.mock.calls[0][1];
+            expect((writtenContent.match(/@since 2.0.0/g) || []).length).toBe(2);
+        });
+
+        test('only updates if changes needed', () => {
+            const content = `
+                /**
+                 * @since 1.0.0
+                 */
+                function test() {}
+            `;
+
+            fs.readFileSync.mockReturnValue(content);
+
+            updateSinceTags(mockFilePath, '2.0.0');
+
+            expect(fs.writeFileSync).not.toHaveBeenCalled();
+        });
+
+        test('replaces all placeholder variants with version', () => {
+            fs.readFileSync.mockReturnValue(`
+                /**
+                 * @since todo
+                 */
+                function test1() {}
+
+                /**
+                 * @since next-version
+                 */
+                function test2() {}
+
+                /**
+                 * @since tbd
+                 */
+                function test3() {}
+
+                /**
+                 * @next-version
+                 */
+                function test4() {}
+            `);
+
+            updateSinceTags(mockFilePath, '2.0.0');
+
+            const writtenContent = fs.writeFileSync.mock.calls[0][1];
+            expect(writtenContent).toContain('@since 2.0.0');
+            expect(writtenContent.match(/@since 2.0.0/g).length).toBe(4);
+            expect(writtenContent).not.toContain('@since todo');
+            expect(writtenContent).not.toContain('@since next-version');
+            expect(writtenContent).not.toContain('@since tbd');
+            expect(writtenContent).not.toContain('@next-version');
+        });
+    });
+});
\ No newline at end of file
diff --git a/scripts/__tests__/version-integration.test.js b/scripts/__tests__/version-integration.test.js
new file mode 100644
index 000000000..b9509dd08
--- /dev/null
+++ b/scripts/__tests__/version-integration.test.js
@@ -0,0 +1,111 @@
+const { updateVersions, getCurrentVersions } = require('../version-management');
+const { updateAllSinceTags } = require('../update-since-tags');
+const fs = require('fs');
+
+jest.mock('fs');
+jest.mock('../update-since-tags');
+
+describe('Version Integration', () => {
+    let mockFiles = {
+        'wp-graphql.php': 'Version: 1.0.0',
+        'constants.php': "define( 'WPGRAPHQL_VERSION', '1.0.0' );",
+        'package.json': JSON.stringify({ version: '1.0.0' }),
+        'readme.txt': 'Stable tag: 1.0.0'
+    };
+
+    beforeEach(() => {
+        jest.clearAllMocks();
+
+        // Reset mock files for each test
+        mockFiles = {
+            'wp-graphql.php': 'Version: 1.0.0',
+            'constants.php': "define( 'WPGRAPHQL_VERSION', '1.0.0' );",
+            'package.json': JSON.stringify({ version: '1.0.0' }),
+            'readme.txt': 'Stable tag: 1.0.0'
+        };
+
+        // Mock file system operations
+        fs.readFileSync.mockImplementation((file) => {
+            const fileName = file.split('/').pop();
+            if (mockFiles[fileName]) {
+                return mockFiles[fileName];
+            }
+            throw new Error(`ENOENT: no such file or directory, open '${file}'`);
+        });
+        fs.existsSync.mockImplementation((file) => {
+            const fileName = file.split('/').pop();
+            return !!mockFiles[fileName];
+        });
+        fs.writeFileSync.mockImplementation((file, content) => {
+            const fileName = file.split('/').pop();
+            mockFiles[fileName] = content;
+        });
+
+        // Mock @since tag updates
+        updateAllSinceTags.mockResolvedValue({ updated: [], errors: [] });
+    });
+
+    describe('Stable Release', () => {
+        test('updates all version numbers for stable release', async () => {
+            const newVersion = '2.0.0';
+            await updateVersions(newVersion, false);
+
+            // Check that all files were updated
+            expect(mockFiles['wp-graphql.php']).toContain(`Version: ${newVersion}`);
+            expect(mockFiles['constants.php']).toContain(`'WPGRAPHQL_VERSION', '${newVersion}'`);
+            expect(mockFiles['package.json']).toContain(`"version": "${newVersion}"`);
+            expect(mockFiles['readme.txt']).toContain(`Stable tag: ${newVersion}`);
+        });
+
+        test('updates @since tags with new version', async () => {
+            const newVersion = '2.0.0';
+            await updateVersions(newVersion, false);
+            expect(updateAllSinceTags).toHaveBeenCalledWith(newVersion);
+        });
+    });
+
+    describe('Beta Release', () => {
+        test('does not update stable tag in readme.txt for beta release', async () => {
+            const newVersion = '2.0.0-beta.1';
+            await updateVersions(newVersion, true);
+
+            // Check that readme.txt stable tag was not updated
+            expect(mockFiles['readme.txt']).toContain('Stable tag: 1.0.0');
+
+            // Check that other files were updated
+            expect(mockFiles['wp-graphql.php']).toContain(`Version: ${newVersion}`);
+            expect(mockFiles['constants.php']).toContain(`'WPGRAPHQL_VERSION', '${newVersion}'`);
+            expect(mockFiles['package.json']).toContain(`"version": "${newVersion}"`);
+        });
+
+        test('updates @since tags with beta version', async () => {
+            const newVersion = '2.0.0-beta.1';
+            await updateVersions(newVersion, true);
+            expect(updateAllSinceTags).toHaveBeenCalledWith(newVersion);
+        });
+    });
+
+    describe('Version Validation', () => {
+        test('validates versions match after update', async () => {
+            const newVersion = '2.0.0';
+            await updateVersions(newVersion, false);
+
+            const versions = getCurrentVersions();
+            expect(versions.php).toBe(newVersion);
+            expect(versions.constants).toBe(newVersion);
+            expect(versions.package).toBe(newVersion);
+            expect(versions.readme).toBe(newVersion);
+        });
+
+        test('validates beta versions correctly', async () => {
+            const newVersion = '2.0.0-beta.1';
+            await updateVersions(newVersion, true);
+
+            const versions = getCurrentVersions();
+            expect(versions.php).toBe(newVersion);
+            expect(versions.constants).toBe(newVersion);
+            expect(versions.package).toBe(newVersion);
+            expect(versions.readme).toBe('1.0.0'); // Should keep old stable tag
+        });
+    });
+});
\ No newline at end of file
diff --git a/scripts/__tests__/version-management.test.js b/scripts/__tests__/version-management.test.js
new file mode 100644
index 000000000..b13a5a0dc
--- /dev/null
+++ b/scripts/__tests__/version-management.test.js
@@ -0,0 +1,154 @@
+const { updateVersions, getCurrentVersions, validateVersions, resetVersions } = require('../version-management');
+const fs = require('fs');
+
+jest.mock('fs');
+jest.mock('../update-since-tags', () => ({
+    updateAllSinceTags: jest.fn().mockResolvedValue({ updated: [], errors: [] })
+}));
+
+describe('Version Management', () => {
+    let mockFiles;
+
+    beforeEach(() => {
+        jest.clearAllMocks();
+
+        // Set up initial mock files
+        mockFiles = {
+            'wp-graphql.php': 'Version: 2.0.0',
+            'constants.php': "define( 'WPGRAPHQL_VERSION', '2.0.0' );",
+            'package.json': JSON.stringify({ version: '2.0.0' }),
+            'readme.txt': 'Stable tag: 2.0.0'
+        };
+
+        // Mock file system operations
+        fs.readFileSync.mockImplementation((file) => {
+            const fileName = file.split('/').pop();
+            if (mockFiles[fileName]) {
+                return mockFiles[fileName];
+            }
+            throw new Error('File not found');
+        });
+
+        fs.writeFileSync.mockImplementation((file, content) => {
+            const fileName = file.split('/').pop();
+            mockFiles[fileName] = content;
+        });
+
+        fs.existsSync.mockImplementation((file) => {
+            const fileName = file.split('/').pop();
+            return !!mockFiles[fileName];
+        });
+    });
+
+    describe('getCurrentVersions', () => {
+        test('reads versions from all files', () => {
+            const versions = getCurrentVersions();
+            expect(versions).toEqual({
+                php: '2.0.0',
+                constants: '2.0.0',
+                package: '2.0.0',
+                readme: '2.0.0'
+            });
+        });
+
+        test('handles missing files', () => {
+            mockFiles = {}; // No files exist
+            const versions = getCurrentVersions();
+            expect(versions).toEqual({
+                php: '2.1.0',
+                constants: '2.1.0',
+                package: '2.1.0',
+                readme: '2.1.0'
+            });
+        });
+
+        test('handles invalid versions', () => {
+            mockFiles['wp-graphql.php'] = 'Version: invalid';
+            const versions = getCurrentVersions();
+            expect(versions.php).toBe('2.1.0');
+        });
+    });
+
+    describe('validateVersions', () => {
+        test('passes when versions match', () => {
+            const versions = {
+                php: '2.0.0',
+                constants: '2.0.0',
+                package: '2.0.0',
+                readme: '2.0.0'
+            };
+            expect(validateVersions(versions)).toBe(true);
+        });
+
+        test('fails when versions mismatch', () => {
+            const versions = {
+                php: '2.0.0',
+                constants: '2.0.1',
+                package: '2.0.0',
+                readme: '2.0.0'
+            };
+            expect(() => validateVersions(versions)).toThrow('Version mismatch');
+        });
+
+        test('handles beta versions correctly', () => {
+            const versions = {
+                php: '2.0.0-beta.1',
+                constants: '2.0.0-beta.1',
+                package: '2.0.0-beta.1',
+                readme: '2.0.0' // Should keep old stable tag
+            };
+            expect(validateVersions(versions)).toBe(true);
+        });
+    });
+
+    describe('updateVersions', () => {
+        test('updates all files for stable release', async () => {
+            await updateVersions('2.1.0');
+
+            expect(mockFiles['wp-graphql.php']).toContain('Version: 2.1.0');
+            expect(mockFiles['constants.php']).toContain("'WPGRAPHQL_VERSION', '2.1.0'");
+            expect(mockFiles['package.json']).toContain('"version": "2.1.0"');
+            expect(mockFiles['readme.txt']).toContain('Stable tag: 2.1.0');
+        });
+
+        test('handles beta releases correctly', async () => {
+            await updateVersions('2.1.0-beta.1', true);
+
+            expect(mockFiles['wp-graphql.php']).toContain('Version: 2.1.0-beta.1');
+            expect(mockFiles['constants.php']).toContain("'WPGRAPHQL_VERSION', '2.1.0-beta.1'");
+            expect(mockFiles['package.json']).toContain('"version": "2.1.0-beta.1"');
+            expect(mockFiles['readme.txt']).toContain('Stable tag: 2.0.0'); // Should not update
+        });
+
+        test('prevents large version jumps', async () => {
+            await expect(updateVersions('4.0.0')).rejects.toThrow('Version jump too large');
+        });
+    });
+
+    describe('resetVersions', () => {
+        test('resets all versions to specified version', () => {
+            const result = resetVersions('2.5.0');
+            expect(result).toEqual({
+                php: '2.5.0',
+                constants: '2.5.0',
+                package: '2.5.0',
+                readme: '2.5.0'
+            });
+        });
+
+        test('handles missing files gracefully', () => {
+            mockFiles = {}; // No files exist
+            const result = resetVersions('2.5.0');
+            expect(result).toEqual({
+                php: '2.5.0',
+                constants: '2.5.0',
+                package: '2.5.0',
+                readme: '2.5.0'
+            });
+        });
+
+        test('validates version format', () => {
+            expect(() => resetVersions('invalid')).toThrow('Invalid version format');
+        });
+    });
+});
\ No newline at end of file