Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(deps): update dependency semver to v7.7.1 #66

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

renovate[bot]
Copy link
Contributor

@renovate renovate bot commented Mar 27, 2024

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
semver 7.5.3 -> 7.7.1 age adoption passing confidence

Release Notes

npm/node-semver (semver)

v7.7.1

Compare Source

Bug Fixes

v7.7.0

Compare Source

Features
Bug Fixes
Documentation
Chores

v7.6.3

Compare Source

Bug Fixes
Documentation

v7.6.2

Compare Source

Bug Fixes

v7.6.1

Compare Source

Bug Fixes
Dependencies
Chores

v7.6.0

Compare Source

Features
Chores

v7.5.4

Compare Source

Bug Fixes

Configuration

📅 Schedule: Branch creation - "* 0-12 * * 3" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

Copy link

[puLL-Merge] - npm/[email protected]

Description

This PR introduces a series of changes aiming at improving the code quality, security, and maintainability of the node-semver package. The modifications range from enhancing ESLint configurations, adding GitHub Actions for checking and installing npm, modifying the dependabot configuration to handle more systematic updates, refining the repository settings, particularly around pull request handling and branch protections, and updating workflows to use newer node versions and improve CI/CD processes.

Changes

Changes

.eslintrc.js

  • Added ignorePatterns to exclude 'tap-testdir*' directories from linting processes.

.github/actions/create-check/action.yml (New File)

  • Implements a new GitHub Action for creating checks, especially useful for CI/CD pipelines.

.github/actions/install-latest-npm/action.yml (New File)

  • Added a new GitHub action for installing the latest compatible version of npm according to the Node version in use.

.github/dependabot.yml

  • Adjustments to dependabot configuration to specify target-branch for updates and extend the scope to additional branches ('release/v5', 'release/v6') with specific rules for each.

.github/settings.yml

  • Introduced more granular branch protection rules and restrictions, including requirements for pull request reviews, code owner reviews, and branch creation blocking for main, release/v5, and release/v6 branches.

GitHub Workflows (.github/workflows/*.yml)

  • Significant updates across various workflows (audit.yml, ci-release.yml, ci.yml, codeql-analysis.yml, post-dependabot.yml, pull-request.yml, release-integration.yml, release.yml):
    • Upgraded Node versions to 20.x in several workflows.
    • Streamlined the use of newly introduced GitHub actions, like install-latest-npm.
    • Adjusted to new approaches for creating checks and handling PR comments.
    • Introduced 'release-integration.yml' as a new workflow for handling release integrations.

.gitignore

  • Added rule to ignore 'tap-testdir*' directories.
  • Added exclusion for 'tsconfig.json'.

.release-please-manifest.json

  • Version bump to "7.6.0".

CHANGELOG.md

  • Documented changes for the new version.

README.md

  • Updates to documentation reflecting new coercing behavior for pre-release and build parts of versions.

JavaScript Files (*.js in various directories)

  • Various code adjustments and improvements to match the updated configurations and workflows.
    • This includes changes to regex patterns, SemVer parsing logic, coercing functions, test cases, and others, which reflect enhanced functionality or code quality improvements.

Security Hotspots

  1. GitHub Actions Security: The inclusion of new GitHub Actions scripts (create-check, install-latest-npm) opens potential vectors for security concerns if the actions are not properly secured, especially regarding the use of secrets.
    Risk: Medium
    Files:

    • .github/actions/create-check/action.yml
    • .github/actions/install-latest-npm/action.yml
  2. Dependabot Configuration: Extensive changes to the dependabot configuration may inadvertently introduce security issues if dependency updates are not properly vetted or if automatic PR merging is enabled without human review.
    Risk: Low
    File: .github/dependabot.yml

  3. External GitHub Action Usage: The workflows use external GitHub actions (LouisBrunner/[email protected], actions/github-script@v6). While common, reliance on third-party actions carries inherent risk if those actions become compromised.
    Risk: Medium
    Example Files:

    • .github/workflows/audit.yml
    • .github/workflows/ci-release.yml
  4. Privilege Escalation via Workflow Misconfigurations: The new or modified GitHub workflows should be carefully reviewed to ensure they do not inadvertently provide escalated privileges to GitHub actions, particularly through the misuse of GitHub secrets or the execution environment.
    Risk: Medium
    Example File: .github/workflows/release-integration.yml

Review of these areas is recommended to ensure they adhere to best security practices and do not introduce unintended vulnerabilities into the project.

@renovate renovate bot force-pushed the renovate/semver-7.x branch from 5ec1e3f to 134fddd Compare May 11, 2024 16:49
@renovate renovate bot changed the title Update dependency semver to v7.6.0 Update dependency semver to v7.6.1 May 11, 2024
Copy link

[puLL-Merge] - npm/[email protected]

Description

This PR updates the semver package to version 7.6.1. It includes bug fixes, dependency updates, and chore updates.

Changes

Changes

  • .commitlintrc.js: Updates commitlint rules to be less strict on subject case and body line length.
  • .eslintrc.js: Adds tap-testdir*/ to ignorePatterns.
  • .github/actions/create-check/action.yml: Adds a new GitHub action to create checks.
  • .github/actions/install-latest-npm/action.yml: Adds a new GitHub action to install the latest compatible npm version.
  • .github/dependabot.yml: Configures Dependabot to target release/* branches and limit open PRs.
  • .github/settings.yml: Updates branch protections and required reviews.
  • .github/workflows/: Updates all workflows to use the new GitHub actions, test on more Node versions and macOS 13. Adds release-integration.yml.
  • .gitignore: Adds tap-testdir*/ and /benchmarks.
  • CHANGELOG.md: Adds entries for 7.6.1, 7.6.0 and 7.5.4 releases.
  • README.md: Various improvements and updates.
  • benchmarks/: Adds benchmark scripts.
  • bin/semver.js: Refactors semver CLI to be more efficient.
  • internal/re.js: Adds support for long build IDs.
  • package.json: Bumps version to 7.6.1, updates dependencies, refines npm scripts.
  • release-please-config.json: Configures Release Please bot.
  • Multiple test files: Adds new tests, makes some existing tests stricter.

Security Hotspots

None found. The changes look safe and do not introduce any obvious security vulnerabilities.

Overall this is a solid maintenance release with useful improvements and fixes. The new features like supporting prerelease/build parts in coerce are well tested. Adding benchmark scripts is a nice enhancement too.

Let me know if you have any other questions! The PR looks good to merge from my review.

@renovate renovate bot force-pushed the renovate/semver-7.x branch from 134fddd to 7eb30fe Compare May 13, 2024 16:34
@renovate renovate bot changed the title Update dependency semver to v7.6.1 Update dependency semver to v7.6.2 May 13, 2024
Copy link

[puLL-Merge] - npm/[email protected]

Description

This PR updates the semver library to version 7.6.2. The main changes include:

  • Fixing various bugs related to comparisons, parsing, and the lru-cache dependency
  • Adding new benchmarking tests
  • Upgrading dependencies like @npmcli/template-oss
  • Improving documentation in the README
  • Preserving prerelease and build parts when using coerce() with the includePrerelease option

The motivation appears to be fixing bugs, improving performance, and adding some new functionality while keeping the library up-to-date.

Changes

Changes

  • .commitlintrc.js: Relaxed some commitlint rules around subject case and body line length
  • .eslintrc.js: Added tap-testdir*/ to ignored patterns
  • .github/:
    • Added new reusable workflow for creating checks
    • Added new reusable workflow for installing latest compatible npm version
    • Updated dependabot config to support older release branches
    • Other workflow updates and improvements
  • .gitignore: Added tap-testdir*/ to ignored paths
  • README.md: Various documentation improvements and updates
  • benchmarks/: Added new benchmark tests for comparing, parsing, satisfying versions
  • bin/semver.js: Refactored success function logic directly into main
  • classes/range.js: Switched to use internal LRUCache, call trim() on ranges
  • classes/semver.js: Removed test for invalid version numbers
  • functions/coerce.js: Support includePrerelease option to preserve prerelease and build parts
  • internal/: Added custom LRUCache implementation, removed lru-cache dependency
  • package.json: Version bump to 7.6.2, removed lru-cache dependency
  • test/: Various test additions and updates

Security Hotspots

None found. The changes appear to be safe bug fixes, refactorings and enhancements. Switching to a custom LRUCache implementation removes a third-party dependency.

Let me know if you have any other questions!

@renovate renovate bot force-pushed the renovate/semver-7.x branch from 7eb30fe to dd958a7 Compare July 20, 2024 22:58
@renovate renovate bot changed the title Update dependency semver to v7.6.2 Update dependency semver to v7.6.3 Jul 20, 2024
Copy link

[puLL-Merge] - npm/[email protected]

Description

This PR updates the node-semver package with several improvements and changes across multiple files. The changes include performance optimizations, new features, bug fixes, and updates to the project configuration.

Changes

Changes

  1. .commitlintrc.js:

    • Simplified subject-case rule
    • Added body-max-line-length rule
  2. .eslintrc.js:

    • Added ignore pattern for tap-testdir
  3. .github/actions/:

    • Added new GitHub Actions for creating checks and installing the latest npm version
  4. .github/dependabot.yml:

    • Updated configuration for dependency management
  5. .github/settings.yml:

    • Updated branch protection rules
  6. .github/workflows/:

    • Updated CI, release, and audit workflows
  7. .gitignore:

    • Added exclusion for tap-testdir and included benchmarks directory
  8. CHANGELOG.md:

    • Updated with new versions and changes
  9. README.md:

    • Improved documentation and examples
  10. benchmarks/:

    • Added new benchmark files for various functions
  11. bin/semver.js:

    • Refactored main function for better performance
  12. classes/range.js:

    • Optimized Range parsing and formatting
  13. classes/semver.js:

    • Minor updates to debug messages
  14. functions/coerce.js:

    • Enhanced coercion functionality
  15. internal/lrucache.js:

    • Implemented a custom LRU cache
  16. internal/re.js:

    • Updated regular expressions for version parsing
  17. package.json:

    • Updated version to 7.6.3
    • Updated dependencies and dev dependencies
    • Removed lru-cache dependency
  18. release-please-config.json:

    • Updated release configuration
  19. test/:

    • Added and updated various test cases

Possible Issues

  • The removal of the lru-cache dependency and implementation of a custom LRU cache might introduce performance differences that should be carefully monitored.

Security Hotspots

No significant security issues were identified in this PR.

@renovate renovate bot changed the title Update dependency semver to v7.6.3 chore(deps): update dependency semver to v7.6.3 Oct 8, 2024
@renovate renovate bot changed the title chore(deps): update dependency semver to v7.6.3 chore(deps): update dependency semver to v7.7.0 Feb 5, 2025
@renovate renovate bot force-pushed the renovate/semver-7.x branch from dd958a7 to 165e93d Compare February 5, 2025 17:57
Copy link

github-actions bot commented Feb 5, 2025

[puLL-Merge] - npm/[email protected]

Diff
diff --git .commitlintrc.js .commitlintrc.js
index 5b0b1a52..b706e527 100644
--- .commitlintrc.js
+++ .commitlintrc.js
@@ -5,6 +5,8 @@ module.exports = {
   rules: {
     'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']],
     'header-max-length': [2, 'always', 80],
-    'subject-case': [0, 'always', ['lower-case', 'sentence-case', 'start-case']],
+    'subject-case': [0],
+    'body-max-line-length': [0],
+    'footer-max-line-length': [0],
   },
 }
diff --git .eslintrc.js .eslintrc.js
index 5db9f815..f21d26ec 100644
--- .eslintrc.js
+++ .eslintrc.js
@@ -10,6 +10,9 @@ const localConfigs = readdir(__dirname)
 
 module.exports = {
   root: true,
+  ignorePatterns: [
+    'tap-testdir*/',
+  ],
   extends: [
     '@npmcli',
     ...localConfigs,
diff --git a/.github/actions/create-check/action.yml b/.github/actions/create-check/action.yml
new file mode 100644
index 00000000..d1220c90
--- /dev/null
+++ .github/actions/create-check/action.yml
@@ -0,0 +1,52 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: 'Create Check'
+inputs:
+  name:
+    required: true
+  token:
+    required: true
+  sha:
+    required: true
+  check-name:
+    default: ''
+outputs:
+  check-id:
+    value: ${{ steps.create-check.outputs.check_id }}
+runs:
+  using: "composite"
+  steps:
+    - name: Get Workflow Job
+      uses: actions/github-script@v7
+      id: workflow
+      env:
+        JOB_NAME: "${{ inputs.name }}"
+        SHA: "${{ inputs.sha }}"
+      with:
+        result-encoding: string
+        script: |
+          const { repo: { owner, repo}, runId, serverUrl } = context
+          const { JOB_NAME, SHA } = process.env
+
+          const job = await github.rest.actions.listJobsForWorkflowRun({
+            owner,
+            repo,
+            run_id: runId,
+            per_page: 100
+          }).then(r => r.data.jobs.find(j => j.name.endsWith(JOB_NAME)))
+
+          return [
+            `This check is assosciated with ${serverUrl}/${owner}/${repo}/commit/${SHA}.`,
+            'Run logs:',
+            job?.html_url || `could not be found for a job ending with: "${JOB_NAME}"`,
+          ].join(' ')
+    - name: Create Check
+      uses: LouisBrunner/[email protected]
+      id: create-check
+      with:
+        token: ${{ inputs.token }}
+        sha: ${{ inputs.sha }}
+        status: in_progress
+        name: ${{ inputs.check-name || inputs.name }}
+        output: |
+          {"summary":"${{ steps.workflow.outputs.result }}"}
diff --git a/.github/actions/install-latest-npm/action.yml b/.github/actions/install-latest-npm/action.yml
new file mode 100644
index 00000000..580603dd
--- /dev/null
+++ .github/actions/install-latest-npm/action.yml
@@ -0,0 +1,58 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: 'Install Latest npm'
+description: 'Install the latest version of npm compatible with the Node version'
+inputs:
+  node:
+    description: 'Current Node version'
+    required: true
+runs:
+  using: "composite"
+  steps:
+    # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows
+    - name: Update Windows npm
+      if: |
+        runner.os == 'Windows' && (
+          startsWith(inputs.node, 'v10.') ||
+          startsWith(inputs.node, 'v12.') ||
+          startsWith(inputs.node, 'v14.')
+        )
+      shell: cmd
+      run: |
+        curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
+        tar xf npm-7.5.4.tgz
+        cd package
+        node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
+        cd ..
+        rmdir /s /q package
+    - name: Install Latest npm
+      shell: bash
+      env:
+        NODE_VERSION: ${{ inputs.node }}
+      working-directory: ${{ runner.temp }}
+      run: |
+        MATCH=""
+        SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6")
+
+        echo "node@$NODE_VERSION"
+
+        for SPEC in ${SPECS[@]}; do
+          ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node')
+          echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)"
+
+          if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then
+            MATCH=$SPEC
+            echo "Found compatible version: npm@$MATCH"
+            break
+          fi
+        done
+
+        if [ -z $MATCH ]; then
+          echo "Could not find a compatible version of npm for node@$NODE_VERSION"
+          exit 1
+        fi
+
+        npm i --prefer-online --no-fund --no-audit -g npm@$MATCH
+    - name: npm Version
+      shell: bash
+      run: npm -v
diff --git .github/dependabot.yml .github/dependabot.yml
index 8da2a452..d735ccf2 100644
--- .github/dependabot.yml
+++ .github/dependabot.yml
@@ -7,6 +7,7 @@ updates:
     directory: /
     schedule:
       interval: daily
+    target-branch: "main"
     allow:
       - dependency-type: direct
     versioning-strategy: increase-if-necessary
@@ -15,3 +16,38 @@ updates:
       prefix-development: chore
     labels:
       - "Dependencies"
+    open-pull-requests-limit: 10
+  - package-ecosystem: npm
+    directory: /
+    schedule:
+      interval: daily
+    target-branch: "release/v5"
+    allow:
+      - dependency-type: direct
+        dependency-name: "@npmcli/template-oss"
+    versioning-strategy: increase-if-necessary
+    commit-message:
+      prefix: deps
+      prefix-development: chore
+    labels:
+      - "Dependencies"
+      - "Backport"
+      - "release/v5"
+    open-pull-requests-limit: 10
+  - package-ecosystem: npm
+    directory: /
+    schedule:
+      interval: daily
+    target-branch: "release/v6"
+    allow:
+      - dependency-type: direct
+        dependency-name: "@npmcli/template-oss"
+    versioning-strategy: increase-if-necessary
+    commit-message:
+      prefix: deps
+      prefix-development: chore
+    labels:
+      - "Dependencies"
+      - "Backport"
+      - "release/v6"
+    open-pull-requests-limit: 10
diff --git .github/settings.yml .github/settings.yml
index 107aa0ad..206b6eeb 100644
--- .github/settings.yml
+++ .github/settings.yml
@@ -15,6 +15,35 @@ branches:
     protection:
       required_status_checks: null
       enforce_admins: true
+      block_creations: true
+      required_pull_request_reviews:
+        required_approving_review_count: 1
+        require_code_owner_reviews: true
+        require_last_push_approval: true
+        dismiss_stale_reviews: true
+      restrictions:
+        apps: []
+        users: []
+        teams: [ "cli-team" ]
+  - name: release/v5
+    protection:
+      required_status_checks: null
+      enforce_admins: true
+      block_creations: true
+      required_pull_request_reviews:
+        required_approving_review_count: 1
+        require_code_owner_reviews: true
+        require_last_push_approval: true
+        dismiss_stale_reviews: true
+      restrictions:
+        apps: []
+        users: []
+        teams: [ "cli-team" ]
+  - name: release/v6
+    protection:
+      required_status_checks: null
+      enforce_admins: true
+      block_creations: true
       required_pull_request_reviews:
         required_approving_review_count: 1
         require_code_owner_reviews: true
diff --git .github/workflows/audit.yml .github/workflows/audit.yml
index 8b8f3748..a3ae7257 100644
--- .github/workflows/audit.yml
+++ .github/workflows/audit.yml
@@ -18,19 +18,21 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund --package-lock
       - name: Run Production Audit
diff --git .github/workflows/ci-release.yml .github/workflows/ci-release.yml
index 98b70866..f6ab948b 100644
--- .github/workflows/ci-release.yml
+++ .github/workflows/ci-release.yml
@@ -27,65 +27,32 @@ jobs:
       run:
         shell: bash
     steps:
-      - name: Get Workflow Job
-        uses: actions/github-script@v6
-        if: inputs.check-sha
-        id: check-output
-        env:
-          JOB_NAME: "Lint All"
-          MATRIX_NAME: ""
-        with:
-          script: |
-            const { owner, repo } = context.repo
-
-            const { data } = await github.rest.actions.listJobsForWorkflowRun({
-              owner,
-              repo,
-              run_id: context.runId,
-              per_page: 100
-            })
-
-            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
-            const job = data.jobs.find(j => j.name.endsWith(jobName))
-            const jobUrl = job?.html_url
-
-            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
-
-            let summary = `This check is assosciated with ${shaUrl}\n\n`
-
-            if (jobUrl) {
-              summary += `For run logs, click here: ${jobUrl}`
-            } else {
-              summary += `Run logs could not be found for a job with name: "${jobName}"`
-            }
-
-            return { summary }
-      - name: Create Check
-        uses: LouisBrunner/[email protected]
-        id: check
-        if: inputs.check-sha
-        with:
-          token: ${{ secrets.GITHUB_TOKEN }}
-          status: in_progress
-          name: Lint All
-          sha: ${{ inputs.check-sha }}
-          output: ${{ steps.check-output.outputs.result }}
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           ref: ${{ inputs.ref }}
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
+      - name: Create Check
+        id: create-check
+        if: ${{ inputs.check-sha }}
+        uses: ./.github/actions/create-check
+        with:
+          name: "Lint All"
+          token: ${{ secrets.GITHUB_TOKEN }}
+          sha: ${{ inputs.check-sha }}
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
+        with:
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Lint
@@ -94,11 +61,11 @@ jobs:
         run: npm run postlint --ignore-scripts
       - name: Conclude Check
         uses: LouisBrunner/[email protected]
-        if: steps.check.outputs.check_id && always()
+        if: steps.create-check.outputs.check-id && always()
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           conclusion: ${{ job.status }}
-          check_id: ${{ steps.check.outputs.check_id }}
+          check_id: ${{ steps.create-check.outputs.check-id }}
 
   test-all:
     name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
@@ -113,6 +80,9 @@ jobs:
           - name: macOS
             os: macos-latest
             shell: bash
+          - name: macOS
+            os: macos-13
+            shell: bash
           - name: Windows
             os: windows-latest
             shell: cmd
@@ -123,84 +93,56 @@ jobs:
           - 14.x
           - 16.x
           - 18.x
+          - 20.x
+          - 22.x
+        exclude:
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 10.0.0
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 10.x
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 12.x
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 14.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 16.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 18.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 20.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 22.x
     runs-on: ${{ matrix.platform.os }}
     defaults:
       run:
         shell: ${{ matrix.platform.shell }}
     steps:
-      - name: Get Workflow Job
-        uses: actions/github-script@v6
-        if: inputs.check-sha
-        id: check-output
-        env:
-          JOB_NAME: "Test All"
-          MATRIX_NAME: " - ${{ matrix.platform.name }} - ${{ matrix.node-version }}"
-        with:
-          script: |
-            const { owner, repo } = context.repo
-
-            const { data } = await github.rest.actions.listJobsForWorkflowRun({
-              owner,
-              repo,
-              run_id: context.runId,
-              per_page: 100
-            })
-
-            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
-            const job = data.jobs.find(j => j.name.endsWith(jobName))
-            const jobUrl = job?.html_url
-
-            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
-
-            let summary = `This check is assosciated with ${shaUrl}\n\n`
-
-            if (jobUrl) {
-              summary += `For run logs, click here: ${jobUrl}`
-            } else {
-              summary += `Run logs could not be found for a job with name: "${jobName}"`
-            }
-
-            return { summary }
-      - name: Create Check
-        uses: LouisBrunner/[email protected]
-        id: check
-        if: inputs.check-sha
-        with:
-          token: ${{ secrets.GITHUB_TOKEN }}
-          status: in_progress
-          name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
-          sha: ${{ inputs.check-sha }}
-          output: ${{ steps.check-output.outputs.result }}
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           ref: ${{ inputs.ref }}
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
+      - name: Create Check
+        id: create-check
+        if: ${{ inputs.check-sha }}
+        uses: ./.github/actions/create-check
+        with:
+          name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}"
+          token: ${{ secrets.GITHUB_TOKEN }}
+          sha: ${{ inputs.check-sha }}
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
           node-version: ${{ matrix.node-version }}
-      - name: Update Windows npm
-        # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
-        if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
-        run: |
-          curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
-          tar xf npm-7.5.4.tgz
-          cd package
-          node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
-          cd ..
-          rmdir /s /q package
-      - name: Install npm@7
-        if: startsWith(matrix.node-version, '10.')
-        run: npm i --prefer-online --no-fund --no-audit -g npm@7
-      - name: Install npm@8
-        if: ${{ !startsWith(matrix.node-version, '10.') }}
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          check-latest: contains(matrix.node-version, '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Add Problem Matcher
@@ -209,8 +151,8 @@ jobs:
         run: npm test --ignore-scripts
       - name: Conclude Check
         uses: LouisBrunner/[email protected]
-        if: steps.check.outputs.check_id && always()
+        if: steps.create-check.outputs.check-id && always()
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           conclusion: ${{ job.status }}
-          check_id: ${{ steps.check.outputs.check_id }}
+          check_id: ${{ steps.create-check.outputs.check-id }}
diff --git .github/workflows/ci.yml .github/workflows/ci.yml
index 90c632b9..4a2724b6 100644
--- .github/workflows/ci.yml
+++ .github/workflows/ci.yml
@@ -8,7 +8,7 @@ on:
   push:
     branches:
       - main
-      - latest
+      - release/v*
   schedule:
     # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1
     - cron: "0 9 * * 1"
@@ -23,19 +23,21 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Lint
@@ -56,6 +58,9 @@ jobs:
           - name: macOS
             os: macos-latest
             shell: bash
+          - name: macOS
+            os: macos-13
+            shell: bash
           - name: Windows
             os: windows-latest
             shell: cmd
@@ -66,39 +71,46 @@ jobs:
           - 14.x
           - 16.x
           - 18.x
+          - 20.x
+          - 22.x
+        exclude:
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 10.0.0
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 10.x
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 12.x
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 14.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 16.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 18.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 20.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 22.x
     runs-on: ${{ matrix.platform.os }}
     defaults:
       run:
         shell: ${{ matrix.platform.shell }}
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
           node-version: ${{ matrix.node-version }}
-      - name: Update Windows npm
-        # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
-        if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
-        run: |
-          curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
-          tar xf npm-7.5.4.tgz
-          cd package
-          node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
-          cd ..
-          rmdir /s /q package
-      - name: Install npm@7
-        if: startsWith(matrix.node-version, '10.')
-        run: npm i --prefer-online --no-fund --no-audit -g npm@7
-      - name: Install npm@8
-        if: ${{ !startsWith(matrix.node-version, '10.') }}
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          check-latest: contains(matrix.node-version, '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Add Problem Matcher
diff --git .github/workflows/codeql-analysis.yml .github/workflows/codeql-analysis.yml
index 66b9498a..f8b17025 100644
--- .github/workflows/codeql-analysis.yml
+++ .github/workflows/codeql-analysis.yml
@@ -6,11 +6,11 @@ on:
   push:
     branches:
       - main
-      - latest
+      - release/v*
   pull_request:
     branches:
       - main
-      - latest
+      - release/v*
   schedule:
     # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1
     - cron: "0 10 * * 1"
@@ -25,14 +25,14 @@ jobs:
       security-events: write
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@v2
+        uses: github/codeql-action/init@v3
         with:
           languages: javascript
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@v2
+        uses: github/codeql-action/analyze@v3
diff --git .github/workflows/post-dependabot.yml .github/workflows/post-dependabot.yml
index 03c85681..1ea8693c 100644
--- .github/workflows/post-dependabot.yml
+++ .github/workflows/post-dependabot.yml
@@ -17,7 +17,7 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           ref: ${{ github.event.pull_request.head.ref }}
       - name: Setup Git User
@@ -25,13 +25,15 @@ jobs:
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Fetch Dependabot Metadata
@@ -47,7 +49,7 @@ jobs:
         id: flags
         run: |
           dependabot_dir="${{ steps.metadata.outputs.directory }}"
-          if [[ "$dependabot_dir" == "/" ]]; then
+          if [[ "$dependabot_dir" == "/" || "$dependabot_dir" == "/main" ]]; then
             echo "workspace=-iwr" >> $GITHUB_OUTPUT
           else
             # strip leading slash from directory so it works as a
diff --git .github/workflows/pull-request.yml .github/workflows/pull-request.yml
index da5779df..7dbdfd41 100644
--- .github/workflows/pull-request.yml
+++ .github/workflows/pull-request.yml
@@ -20,7 +20,7 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
       - name: Setup Git User
@@ -28,23 +28,23 @@ jobs:
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Run Commitlint on Commits
         id: commit
         continue-on-error: true
-        run: |
-          npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }}
+        run: npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }}
       - name: Run Commitlint on PR Title
         if: steps.commit.outcome == 'failure'
         env:
           PR_TITLE: ${{ github.event.pull_request.title }}
-        run: |
-          echo '$PR_TITLE' | npx --offline commitlint -V
+        run: echo "$PR_TITLE" | npx --offline commitlint -V
diff --git a/.github/workflows/release-integration.yml b/.github/workflows/release-integration.yml
new file mode 100644
index 00000000..130578e6
--- /dev/null
+++ .github/workflows/release-integration.yml
@@ -0,0 +1,70 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Release Integration
+
+on:
+  workflow_dispatch:
+    inputs:
+      releases:
+        required: true
+        type: string
+        description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version'
+  workflow_call:
+    inputs:
+      releases:
+        required: true
+        type: string
+        description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version'
+    secrets:
+      PUBLISH_TOKEN:
+        required: true
+
+jobs:
+  publish:
+    name: Publish
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    permissions:
+      id-token: write
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          ref: ${{ fromJSON(inputs.releases)[0].tagName }}
+      - name: Setup Git User
+        run: |
+          git config --global user.email "[email protected]"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v4
+        id: node
+        with:
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Set npm authToken
+        run: npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN}
+      - name: Publish
+        env:
+          PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
+          RELEASES: ${{ inputs.releases }}
+        run: |
+          EXIT_CODE=0
+
+          for release in $(echo $RELEASES | jq -r '.[] | @base64'); do
+            PUBLISH_TAG=$(echo "$release" | base64 --decode | jq -r .publishTag)
+            npm publish --provenance --tag="$PUBLISH_TAG"
+            STATUS=$?
+            if [[ "$STATUS" -eq 1 ]]; then
+              EXIT_CODE=$STATUS
+            fi
+          done
+
+          exit $EXIT_CODE
diff --git .github/workflows/release.yml .github/workflows/release.yml
index 3b69ae10..e77e76f2 100644
--- .github/workflows/release.yml
+++ .github/workflows/release.yml
@@ -3,15 +3,9 @@
 name: Release
 
 on:
-  workflow_dispatch:
-    inputs:
-      release-pr:
-        description: a release PR number to rerun release jobs on
-        type: string
   push:
     branches:
       - main
-      - latest
       - release/v*
 
 permissions:
@@ -23,12 +17,12 @@ jobs:
   release:
     outputs:
       pr: ${{ steps.release.outputs.pr }}
-      release: ${{ steps.release.outputs.release }}
-      releases: ${{ steps.release.outputs.releases }}
-      branch: ${{ steps.release.outputs.pr-branch }}
+      pr-branch: ${{ steps.release.outputs.pr-branch }}
       pr-number: ${{ steps.release.outputs.pr-number }}
-      comment-id: ${{ steps.pr-comment.outputs.result }}
-      check-id: ${{ steps.check.outputs.check_id }}
+      pr-sha: ${{ steps.release.outputs.pr-sha }}
+      releases: ${{ steps.release.outputs.releases }}
+      comment-id: ${{ steps.create-comment.outputs.comment-id || steps.update-comment.outputs.comment-id }}
+      check-id: ${{ steps.create-check.outputs.check-id }}
     name: Release
     if: github.repository_owner == 'npm'
     runs-on: ubuntu-latest
@@ -37,108 +31,75 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
+        with:
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Release Please
         id: release
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        run: |
-          npx --offline template-oss-release-please "${{ github.ref_name }}" "${{ inputs.release-pr }}"
-      - name: Post Pull Request Comment
+        run: npx --offline template-oss-release-please --branch="${{ github.ref_name }}" --backport="" --defaultTag="latest"
+      - name: Create Release Manager Comment Text
         if: steps.release.outputs.pr-number
-        uses: actions/github-script@v6
-        id: pr-comment
-        env:
-          PR_NUMBER: ${{ steps.release.outputs.pr-number }}
-          REF_NAME: ${{ github.ref_name }}
+        uses: actions/github-script@v7
+        id: comment-text
         with:
+          result-encoding: string
           script: |
-            const { REF_NAME, PR_NUMBER: issue_number } = process.env
             const { runId, repo: { owner, repo } } = context
-
             const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId })
-
-            let body = '## Release Manager\n\n'
-
-            const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
-            let commentId = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id
-
-            body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n`
-            body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`main\`. `
-            body += `To force CI to update this PR, run this command:\n\n`
-            body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${issue_number}\n\`\`\``
-
-            if (commentId) {
-              await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body })
-            } else {
-              const { data: comment } = await github.rest.issues.createComment({ owner, repo, issue_number, body })
-              commentId = comment?.id
-            }
-
-            return commentId
-      - name: Get Workflow Job
-        uses: actions/github-script@v6
-        if: steps.release.outputs.pr-sha
-        id: check-output
-        env:
-          JOB_NAME: "Release"
-          MATRIX_NAME: ""
+            return['## Release Manager', `Release workflow run: ${workflow.html_url}`].join('\n\n')
+      - name: Find Release Manager Comment
+        uses: peter-evans/find-comment@v2
+        if: steps.release.outputs.pr-number
+        id: found-comment
         with:
-          script: |
-            const { owner, repo } = context.repo
-
-            const { data } = await github.rest.actions.listJobsForWorkflowRun({
-              owner,
-              repo,
-              run_id: context.runId,
-              per_page: 100
-            })
-
-            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
-            const job = data.jobs.find(j => j.name.endsWith(jobName))
-            const jobUrl = job?.html_url
-
-            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.release.outputs.pr-sha }}`
-
-            let summary = `This check is assosciated with ${shaUrl}\n\n`
-
-            if (jobUrl) {
-              summary += `For run logs, click here: ${jobUrl}`
-            } else {
-              summary += `Run logs could not be found for a job with name: "${jobName}"`
-            }
-
-            return { summary }
+          issue-number: ${{ steps.release.outputs.pr-number }}
+          comment-author: 'github-actions[bot]'
+          body-includes: '## Release Manager'
+      - name: Create Release Manager Comment
+        id: create-comment
+        if: steps.release.outputs.pr-number && !steps.found-comment.outputs.comment-id
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          issue-number: ${{ steps.release.outputs.pr-number }}
+          body: ${{ steps.comment-text.outputs.result }}
+      - name: Update Release Manager Comment
+        id: update-comment
+        if: steps.release.outputs.pr-number && steps.found-comment.outputs.comment-id
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          comment-id: ${{ steps.found-comment.outputs.comment-id }}
+          body: ${{ steps.comment-text.outputs.result }}
+          edit-mode: 'replace'
       - name: Create Check
-        uses: LouisBrunner/[email protected]
-        id: check
+        id: create-check
+        uses: ./.github/actions/create-check
         if: steps.release.outputs.pr-sha
         with:
+          name: "Release"
           token: ${{ secrets.GITHUB_TOKEN }}
-          status: in_progress
-          name: Release
           sha: ${{ steps.release.outputs.pr-sha }}
-          output: ${{ steps.check-output.outputs.result }}
 
   update:
     needs: release
     outputs:
       sha: ${{ steps.commit.outputs.sha }}
-      check-id: ${{ steps.check.outputs.check_id }}
+      check-id: ${{ steps.create-check.outputs.check-id }}
     name: Update - Release
     if: github.repository_owner == 'npm' && needs.release.outputs.pr
     runs-on: ubuntu-latest
@@ -147,32 +108,41 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
-          ref: ${{ needs.release.outputs.branch }}
+          ref: ${{ needs.release.outputs.pr-branch }}
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
+        with:
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Create Release Manager Checklist Text
+        id: comment-text
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: npm exec --offline -- template-oss-release-manager --pr="${{ needs.release.outputs.pr-number }}" --backport="" --defaultTag="latest" --publish
+      - name: Append Release Manager Comment
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          comment-id: ${{ needs.release.outputs.comment-id }}
+          body: ${{ steps.comment-text.outputs.result }}
+          edit-mode: 'append'
       - name: Run Post Pull Request Actions
         env:
-          RELEASE_PR_NUMBER: ${{ needs.release.outputs.pr-number }}
-          RELEASE_COMMENT_ID: ${{ needs.release.outputs.comment-id }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        run: |
-          npm exec --offline -- template-oss-release-manager --lockfile=false --publish=true
-          npm run rp-pull-request --ignore-scripts --if-present
+        run: npm run rp-pull-request --ignore-scripts --if-present -- --pr="${{ needs.release.outputs.pr-number }}" --commentId="${{ needs.release.outputs.comment-id }}"
       - name: Commit
         id: commit
         env:
@@ -181,52 +151,16 @@ jobs:
           git commit --all --amend --no-edit || true
           git push --force-with-lease
           echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-      - name: Get Workflow Job
-        uses: actions/github-script@v6
-        if: steps.commit.outputs.sha
-        id: check-output
-        env:
-          JOB_NAME: "Update - Release"
-          MATRIX_NAME: ""
-        with:
-          script: |
-            const { owner, repo } = context.repo
-
-            const { data } = await github.rest.actions.listJobsForWorkflowRun({
-              owner,
-              repo,
-              run_id: context.runId,
-              per_page: 100
-            })
-
-            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
-            const job = data.jobs.find(j => j.name.endsWith(jobName))
-            const jobUrl = job?.html_url
-
-            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.commit.outputs.sha }}`
-
-            let summary = `This check is assosciated with ${shaUrl}\n\n`
-
-            if (jobUrl) {
-              summary += `For run logs, click here: ${jobUrl}`
-            } else {
-              summary += `Run logs could not be found for a job with name: "${jobName}"`
-            }
-
-            return { summary }
       - name: Create Check
-        uses: LouisBrunner/[email protected]
-        id: check
-        if: steps.commit.outputs.sha
+        id: create-check
+        uses: ./.github/actions/create-check
         with:
+          name: "Update - Release"
+          check-name: "Release"
           token: ${{ secrets.GITHUB_TOKEN }}
-          status: in_progress
-          name: Release
           sha: ${{ steps.commit.outputs.sha }}
-          output: ${{ steps.check-output.outputs.result }}
       - name: Conclude Check
         uses: LouisBrunner/[email protected]
-        if: needs.release.outputs.check-id && always()
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           conclusion: ${{ job.status }}
@@ -238,7 +172,7 @@ jobs:
     if: needs.release.outputs.pr
     uses: ./.github/workflows/ci-release.yml
     with:
-      ref: ${{ needs.release.outputs.branch }}
+      ref: ${{ needs.release.outputs.pr-branch }}
       check-sha: ${{ needs.update.outputs.sha }}
 
   post-ci:
@@ -250,8 +184,8 @@ jobs:
       run:
         shell: bash
     steps:
-      - name: Get Needs Result
-        id: needs-result
+      - name: Get CI Conclusion
+        id: conclusion
         run: |
           result=""
           if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
@@ -264,14 +198,15 @@ jobs:
           echo "result=$result" >> $GITHUB_OUTPUT
       - name: Conclude Check
         uses: LouisBrunner/[email protected]
-        if: needs.update.outputs.check-id && always()
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
-          conclusion: ${{ steps.needs-result.outputs.result }}
+          conclusion: ${{ steps.conclusion.outputs.result }}
           check_id: ${{ needs.update.outputs.check-id }}
 
   post-release:
     needs: release
+    outputs:
+      comment-id: ${{ steps.create-comment.outputs.comment-id }}
     name: Post Release - Release
     if: github.repository_owner == 'npm' && needs.release.outputs.releases
     runs-on: ubuntu-latest
@@ -279,79 +214,54 @@ jobs:
       run:
         shell: bash
     steps:
-      - name: Create Release PR Comment
-        uses: actions/github-script@v6
+      - name: Create Release PR Comment Text
+        id: comment-text
+        uses: actions/github-script@v7
         env:
           RELEASES: ${{ needs.release.outputs.releases }}
         with:
+          result-encoding: string
           script: |
             const releases = JSON.parse(process.env.RELEASES)
             const { runId, repo: { owner, repo } } = context
             const issue_number = releases[0].prNumber
-
-            let body = '## Release Workflow\n\n'
-            for (const { pkgName, version, url } of releases) {
-              body += `- \`${pkgName}@${version}\` ${url}\n`
-            }
-
-            const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
-              .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body })))
-            console.log(`Found comments: ${JSON.stringify(comments, null, 2)}`)
-            const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at'))
-
-            for (const comment of releaseComments) {
-              console.log(`Release comment: ${JSON.stringify(comment, null, 2)}`)
-              await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id })
-            }
-
             const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`
-            await github.rest.issues.createComment({
-              owner,
-              repo,
-              issue_number,
-              body: `${body}- Workflow run: :arrows_counterclockwise: ${runUrl}`,
-            })
+
+            return [
+              '## Release Workflow\n',
+              ...releases.map(r => `- \`${r.pkgName}@${r.version}\` ${r.url}`),
+              `- Workflow run: :arrows_counterclockwise: ${runUrl}`,
+            ].join('\n')
+      - name: Create Release PR Comment
+        id: create-comment
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }}
+          body: ${{ steps.comment-text.outputs.result }}
 
   release-integration:
     needs: release
     name: Release Integration
-    if: needs.release.outputs.release
-    runs-on: ubuntu-latest
-    defaults:
-      run:
-        shell: bash
+    if: needs.release.outputs.releases
+    uses: ./.github/workflows/release-integration.yml
     permissions:
-      deployments: write
       id-token: write
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          ref: ${{ fromJSON(needs.release.outputs.release).tagName }}
-      - name: Setup Node
-        uses: actions/setup-node@v3
-        with:
-          node-version: 18.x
-      - name: Install npm@latest
-        run: |
-          npm i --prefer-online --no-fund --no-audit -g npm@latest
-          npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN}
-      - name: Publish
-        env:
-          PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
-        run: npm publish --provenance
+    secrets:
+      PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
+    with:
+      releases: ${{ needs.release.outputs.releases }}
 
   post-release-integration:
-    needs: [ release, release-integration ]
+    needs: [ release, release-integration, post-release ]
     name: Post Release Integration - Release
-    if: github.repository_owner == 'npm' && needs.release.outputs.release && always()
+    if: github.repository_owner == 'npm' && needs.release.outputs.releases && always()
     runs-on: ubuntu-latest
     defaults:
       run:
         shell: bash
     steps:
-      - name: Get Needs Result
-        id: needs-result
+      - name: Get Post Release Conclusion
+        id: conclusion
         run: |
           if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
             result="x"
@@ -361,39 +271,38 @@ jobs:
             result="white_check_mark"
           fi
           echo "result=$result" >> $GITHUB_OUTPUT
-      - name: Update Release PR Comment
-        uses: actions/github-script@v6
+      - name: Find Release PR Comment
+        uses: peter-evans/find-comment@v2
+        id: found-comment
+        with:
+          issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }}
+          comment-author: 'github-actions[bot]'
+          body-includes: '## Release Workflow'
+      - name: Create Release PR Comment Text
+        id: comment-text
+        if: steps.found-comment.outputs.comment-id
+        uses: actions/github-script@v7
         env:
-          PR_NUMBER: ${{ fromJSON(needs.release.outputs.release).prNumber }}
-          RESULT: ${{ steps.needs-result.outputs.result }}
+          RESULT: ${{ steps.conclusion.outputs.result }}
+          BODY: ${{ steps.found-comment.outputs.comment-body }}
         with:
+          result-encoding: string
           script: |
-            const { PR_NUMBER: issue_number, RESULT } = process.env
-            const { runId, repo: { owner, repo } } = context
-
-            const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
-            const updateComment = comments.find(c =>
-              c.user.login === 'github-actions[bot]' &&
-              c.body.startsWith('## Release Workflow\n\n') &&
-              c.body.includes(runId)
-            )
-
-            if (updateComment) {
-              console.log('Found comment to update:', JSON.stringify(updateComment, null, 2))
-              let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, `Workflow run: :${RESULT}:`)
-              const tagCodeowner = RESULT !== 'white_check_mark'
-              if (tagCodeowner) {
-                body += `\n\n:rotating_light:`
-                body += ` @npm/cli-team: The post-release workflow failed for this release.`
-                body += ` Manual steps may need to be taken after examining the workflow output`
-                body += ` from the above workflow run. :rotating_light:`
-              }
-              await github.rest.issues.updateComment({
-                owner,
-                repo,
-                body,
-                comment_id: updateComment.id,
-              })
-            } else {
-              console.log('No matching comments found:', JSON.stringify(comments, null, 2))
+            const { RESULT, BODY } = process.env
+            const body = [BODY.replace(/(Workflow run: :)[a-z_]+(:)/, `$1${RESULT}$2`)]
+            if (RESULT !== 'white_check_mark') {
+              body.push(':rotating_light::rotating_light::rotating_light:')
+              body.push([
+                '@npm/cli-team: The post-release workflow failed for this release.',
+                'Manual steps may need to be taken after examining the workflow output.'
+              ].join(' '))
+              body.push(':rotating_light::rotating_light::rotating_light:')
             }
+            return body.join('\n\n').trim()
+      - name: Update Release PR Comment
+        if: steps.comment-text.outputs.result
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          comment-id: ${{ steps.found-comment.outputs.comment-id }}
+          body: ${{ steps.comment-text.outputs.result }}
+          edit-mode: 'replace'
diff --git .gitignore .gitignore
index 00bdaf2b..ac3c1dad 100644
--- .gitignore
+++ .gitignore
@@ -3,15 +3,18 @@
 # ignore everything in the root
 /*
 
-# keep these
 !**/.gitignore
 !/.commitlintrc.js
 !/.eslintrc.js
 !/.eslintrc.local.*
+!/.git-blame-ignore-revs
 !/.github/
 !/.gitignore
 !/.npmrc
+!/.prettierignore
+!/.prettierrc.js
 !/.release-please-manifest.json
+!/benchmarks
 !/bin/
 !/CHANGELOG*
 !/classes/
@@ -34,3 +37,5 @@
 !/SECURITY.md
 !/tap-snapshots/
 !/test/
+!/tsconfig.json
+tap-testdir*/
diff --git .release-please-manifest.json .release-please-manifest.json
index cc729eee..6a15549e 100644
--- .release-please-manifest.json
+++ .release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "7.5.3"
+  ".": "7.7.0"
 }
diff --git CHANGELOG.md CHANGELOG.md
index 292ed24a..b4ddfe57 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,5 +1,86 @@
 # Changelog
 
+## [7.7.0](https://github.com/npm/node-semver/compare/v7.6.3...v7.7.0) (2025-01-29)
+### Features
+* [`0864b3c`](https://github.com/npm/node-semver/commit/0864b3ce7932667013e0c7c5ec764777d4682883) [#753](https://github.com/npm/node-semver/pull/753) add "release" inc type (#753) (@mbtools)
+### Bug Fixes
+* [`d588e37`](https://github.com/npm/node-semver/commit/d588e3782864b1cab2fe9f2452b848e8c7f609d1) [#755](https://github.com/npm/node-semver/pull/755) diff: fix prerelease to stable version diff logic (#755) (@eminberkayd, berkay.daglar)
+* [`8a34bde`](https://github.com/npm/node-semver/commit/8a34bdecc783407f4e1a8a1ee1f67906b84a4b78) [#754](https://github.com/npm/node-semver/pull/754) add identifier validation to `inc()` (#754) (@mbtools)
+### Documentation
+* [`67e5478`](https://github.com/npm/node-semver/commit/67e54785a0f871361230f84323cbb631b9b6d834) [#756](https://github.com/npm/node-semver/pull/756) readme: added missing period for consistency (#756) (@shaymolcho)
+* [`868d4bb`](https://github.com/npm/node-semver/commit/868d4bbe3d318c52544f38d5f9977a1103e924c2) [#749](https://github.com/npm/node-semver/pull/749) clarify comment about obsolete prefixes (#749) (@mbtools, @ljharb)
+### Chores
+* [`145c554`](https://github.com/npm/node-semver/commit/145c554b8c7b7ecfcb451153ad18bdb2f24ad10d) [#741](https://github.com/npm/node-semver/pull/741) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (@dependabot[bot])
+* [`753e02b`](https://github.com/npm/node-semver/commit/753e02b9d0cb3ac23e085dc33efcab3e08d61f2b) [#747](https://github.com/npm/node-semver/pull/747) bump @npmcli/template-oss from 4.23.3 to 4.23.4 (#747) (@dependabot[bot], @npm-cli-bot)
+* [`0b812d5`](https://github.com/npm/node-semver/commit/0b812d5fb5fbb208e89dc1250e2efafeaa549437) [#744](https://github.com/npm/node-semver/pull/744) postinstall for dependabot template-oss PR (@hashtagchris)
+
+## [7.6.3](https://github.com/npm/node-semver/compare/v7.6.2...v7.6.3) (2024-07-16)
+
+### Bug Fixes
+
+* [`73a3d79`](https://github.com/npm/node-semver/commit/73a3d79c4ec32d5dd62c9d5f64e5af7fbdad9ec0) [#726](https://github.com/npm/node-semver/pull/726) optimize Range parsing and formatting (#726) (@jviide)
+
+### Documentation
+
+* [`2975ece`](https://github.com/npm/node-semver/commit/2975ece120e17660c9f1ef517de45c09ff821064) [#719](https://github.com/npm/node-semver/pull/719) fix extra backtick typo (#719) (@stdavis)
+
+## [7.6.2](https://github.com/npm/node-semver/compare/v7.6.1...v7.6.2) (2024-05-09)
+
+### Bug Fixes
+
+* [`6466ba9`](https://github.com/npm/node-semver/commit/6466ba9b540252db405fdd2a289dd4651495beea) [#713](https://github.com/npm/node-semver/pull/713) lru: use map.delete() directly (#713) (@negezor, @lukekarrys)
+
+## [7.6.1](https://github.com/npm/node-semver/compare/v7.6.0...v7.6.1) (2024-05-04)
+
+### Bug Fixes
+
+* [`c570a34`](https://github.com/npm/node-semver/commit/c570a348ffc6612af07fe94fa46b9affa5e4eff0) [#704](https://github.com/npm/node-semver/pull/704) linting: no-unused-vars (@wraithgar)
+* [`ad8ff11`](https://github.com/npm/node-semver/commit/ad8ff11dd200dac3a05097d9a82d1977ccfa1535) [#704](https://github.com/npm/node-semver/pull/704) use internal cache implementation (@mbtools)
+* [`ac9b357`](https://github.com/npm/node-semver/commit/ac9b35769ab0ddfefd5a3af4a3ecaf3da2012352) [#682](https://github.com/npm/node-semver/pull/682) typo in compareBuild debug message (#682) (@mbtools)
+
+### Dependencies
+
+* [`988a8de`](https://github.com/npm/node-semver/commit/988a8deb3ea76b9a314a740e66b5fc2f726822f8) [#709](https://github.com/npm/node-semver/pull/709) uninstall `lru-cache` (#709)
+* [`3fabe4d`](https://github.com/npm/node-semver/commit/3fabe4dbfbd199fdb589c076a7f30bc1f18c6614) [#704](https://github.com/npm/node-semver/pull/704) remove lru-cache
+
+### Chores
+
+* [`dd09b60`](https://github.com/npm/node-semver/commit/dd09b60da1e618335d7c269426345b336fd5f63d) [#705](https://github.com/npm/node-semver/pull/705) bump @npmcli/template-oss to 4.22.0 (@lukekarrys)
+* [`ec49cdc`](https://github.com/npm/node-semver/commit/ec49cdcece9db0020d6829b246681ff65a393644) [#701](https://github.com/npm/node-semver/pull/701) chore: chore: postinstall for dependabot template-oss PR (@lukekarrys)
+* [`b236c3d`](https://github.com/npm/node-semver/commit/b236c3d2f357a16a733c96ec2ca8c57848b70091) [#696](https://github.com/npm/node-semver/pull/696) add benchmarks (#696) (@H4ad)
+* [`692451b`](https://github.com/npm/node-semver/commit/692451bd6f75b38a71a99f39da405c94a5954a22) [#688](https://github.com/npm/node-semver/pull/688) various improvements to README (#688) (@mbtools)
+* [`5feeb7f`](https://github.com/npm/node-semver/commit/5feeb7f4f63061e19a29087115b50cb04135b63e) [#705](https://github.com/npm/node-semver/pull/705) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`074156f`](https://github.com/npm/node-semver/commit/074156f64fa91723fe1ae6af8cc497014b9b7aff) [#701](https://github.com/npm/node-semver/pull/701) bump @npmcli/template-oss from 4.21.3 to 4.21.4 (@dependabot[bot])
+
+## [7.6.0](https://github.com/npm/node-semver/compare/v7.5.4...v7.6.0) (2024-01-31)
+
+### Features
+
+* [`a7ab13a`](https://github.com/npm/node-semver/commit/a7ab13a46201e342d34e84a989632b380f755baf) [#671](https://github.com/npm/node-semver/pull/671) preserve pre-release and build parts of a version on coerce (#671) (@madtisa, madtisa, @wraithgar)
+
+### Chores
+
+* [`816c7b2`](https://github.com/npm/node-semver/commit/816c7b2cbfcb1986958a290f941eddfd0441139e) [#667](https://github.com/npm/node-semver/pull/667) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`0bd24d9`](https://github.com/npm/node-semver/commit/0bd24d943cbd1a7f6a2b8d384590bfa98559e1de) [#667](https://github.com/npm/node-semver/pull/667) bump @npmcli/template-oss from 4.21.1 to 4.21.3 (@dependabot[bot])
+* [`e521932`](https://github.com/npm/node-semver/commit/e521932f115a81030f4e7c34e8631cdd3c6a108b) [#652](https://github.com/npm/node-semver/pull/652) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`8873991`](https://github.com/npm/node-semver/commit/88739918080debeb239aae840b35c07436148e50) [#652](https://github.com/npm/node-semver/pull/652) chore: chore: postinstall for dependabot template-oss PR (@lukekarrys)
+* [`f317dc8`](https://github.com/npm/node-semver/commit/f317dc8689781bcfd98e2c32b46157276acdd47c) [#652](https://github.com/npm/node-semver/pull/652) bump @npmcli/template-oss from 4.19.0 to 4.21.0 (@dependabot[bot])
+* [`7303db1`](https://github.com/npm/node-semver/commit/7303db1fe54d6905b23ccb0162878e37d73535ef) [#658](https://github.com/npm/node-semver/pull/658) add clean() test for build metadata (#658) (@jethrodaniel)
+* [`6240d75`](https://github.com/npm/node-semver/commit/6240d75a7c620b0a222f05969a91fdc3dc2be0fb) [#656](https://github.com/npm/node-semver/pull/656) add missing quotes in README.md (#656) (@zyxkad)
+* [`14d263f`](https://github.com/npm/node-semver/commit/14d263faa156e408a033b9b12a2f87735c2df42c) [#625](https://github.com/npm/node-semver/pull/625) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`7c34e1a`](https://github.com/npm/node-semver/commit/7c34e1ac1bcc0bc6579b30745c96075c69bd0332) [#625](https://github.com/npm/node-semver/pull/625) bump @npmcli/template-oss from 4.18.1 to 4.19.0 (@dependabot[bot])
+* [`123e0b0`](https://github.com/npm/node-semver/commit/123e0b03287e1af295ef82d55f55c16805596f35) [#622](https://github.com/npm/node-semver/pull/622) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`737d5e1`](https://github.com/npm/node-semver/commit/737d5e1cf10e631bab8a28594aa2d5c9d4090814) [#622](https://github.com/npm/node-semver/pull/622) bump @npmcli/template-oss from 4.18.0 to 4.18.1 (@dependabot[bot])
+* [`cce6180`](https://github.com/npm/node-semver/commit/cce61804ba6f997225a1267135c06676fe0524d2) [#598](https://github.com/npm/node-semver/pull/598) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`b914a3d`](https://github.com/npm/node-semver/commit/b914a3d0d26ca27d2685053d7d390af4e02eedd9) [#598](https://github.com/npm/node-semver/pull/598) bump @npmcli/template-oss from 4.17.0 to 4.18.0 (@dependabot[bot])
+
+## [7.5.4](https://github.com/npm/node-semver/compare/v7.5.3...v7.5.4) (2023-07-07)
+
+### Bug Fixes
+
+* [`cc6fde2`](https://github.com/npm/node-semver/commit/cc6fde2d34b95cb600d126649d926901bd2a9703) [#588](https://github.com/npm/node-semver/pull/588) trim each range set before parsing (@lukekarrys)
+* [`99d8287`](https://github.com/npm/node-semver/commit/99d8287516a1d2abf0286033e2e26eca6b69c09f) [#583](https://github.com/npm/node-semver/pull/583) correctly parse long build ids as valid (#583) (@lukekarrys)
+
 ## [7.5.3](https://github.com/npm/node-semver/compare/v7.5.2...v7.5.3) (2023-06-22)
 
 ### Bug Fixes
diff --git README.md README.md
index 53ea9b52..e9522153 100644
--- README.md
+++ README.md
@@ -25,7 +25,7 @@ semver.valid(semver.coerce('v2')) // '2.0.0'
 semver.valid(semver.coerce('42.6.7.9.3-alpha')) // '42.6.7'
 \`\`\`
 
-You can also just load the module for the function that you care about, if
+You can also just load the module for the function that you care about if
 you'd like to minimize your footprint.
 
 ```js
@@ -78,8 +78,8 @@ const semverOutside = require('semver/ranges/outside')
 const semverGtr = require('semver/ranges/gtr')
 const semverLtr = require('semver/ranges/ltr')
 const semverIntersects = require('semver/ranges/intersects')
-const simplifyRange = require('semver/ranges/simplify')
-const rangeSubset = require('semver/ranges/subset')
+const semverSimplifyRange = require('semver/ranges/simplify')
+const semverRangeSubset = require('semver/ranges/subset')

As a command-line utility:
@@ -100,7 +100,7 @@ Options:
-i --increment []
Increment a version by the specified level. Level can
be one of: major, minor, patch, premajor, preminor,

  •    prepatch, or prerelease.  Default level is 'patch'.
    
  •    prepatch, prerelease, or release.  Default level is 'patch'.
       Only one version may be specified.
    

--preid
@@ -141,10 +141,12 @@ A "version" is described by the v2.0.0 specification found at
https://semver.org/.

A leading "=" or "v" character is stripped off and ignored.
+Support for stripping a leading "v" is kept for compatibility with v1.0.0 of the SemVer
+specification but should not be used anymore.

Ranges

-A version range is a set of comparators which specify versions
+A version range is a set of comparators that specify versions
that satisfy the range.

A comparator is composed of an operator and a version. The set
@@ -155,7 +157,7 @@ of primitive operators is:

  • > Greater than
  • >= Greater than or equal to
  • = Equal. If no operator is specified, then equality is assumed,
  • so this operator is optional, but MAY be included.
  • so this operator is optional but MAY be included.

For example, the comparator >=1.2.7 would match the versions
1.2.7, 1.2.8, 2.5.3, and 1.3.9, but not the versions 1.2.6
@@ -189,26 +191,26 @@ For example, the range >1.2.3-alpha.3 would be allowed to match the
version 1.2.3-alpha.7, but it would not be satisfied by
3.4.5-alpha.9, even though 3.4.5-alpha.9 is technically "greater
than" 1.2.3-alpha.3 according to the SemVer sort rules. The version
-range only accepts prerelease tags on the 1.2.3 version. The
-version 3.4.5 would satisfy the range, because it does not have a
+range only accepts prerelease tags on the 1.2.3 version.
+Version 3.4.5 would satisfy the range because it does not have a
prerelease flag, and 3.4.5 is greater than 1.2.3-alpha.7.

-The purpose for this behavior is twofold. First, prerelease versions
+The purpose of this behavior is twofold. First, prerelease versions
frequently are updated very quickly, and contain many breaking changes
that are (by the author's design) not yet fit for public consumption.
-Therefore, by default, they are excluded from range matching
+Therefore, by default, they are excluded from range-matching
semantics.

Second, a user who has opted into using a prerelease version has
-clearly indicated the intent to use that specific set of
+indicated the intent to use that specific set of
alpha/beta/rc versions. By including a prerelease tag in the range,
the user is indicating that they are aware of the risk. However, it
is still not appropriate to assume that they have opted into taking a
similar risk on the next set of prerelease versions.

Note that this behavior can be suppressed (treating all prerelease
-versions as if they were normal versions, for the purpose of range
-matching) by setting the includePrerelease flag on the options
+versions as if they were normal versions, for range-matching)
+by setting the includePrerelease flag on the options
object to any
functions that do
range matching.
@@ -237,6 +239,13 @@ $ semver 1.2.4-beta.0 -i prerelease
1.2.4-beta.1


+To get out of the prerelease phase, use the `release` option:
+
+```bash
+$ semver 1.2.4-beta.1 -i release
+1.2.4
+```
+
#### Prerelease Identifier Base

The method `.inc` takes an optional parameter 'identifierBase' string
@@ -401,12 +410,12 @@ All methods and classes take a final `options` object argument.  All
options in this object are `false` by default.  The options supported
are:

-- `loose`  Be more forgiving about not-quite-valid semver strings.
+- `loose`: Be more forgiving about not-quite-valid semver strings.
  (Any resulting output will always be 100% strict compliant, of
  course.)  For backwards compatibility reasons, if the `options`
  argument is a boolean value instead of an object, it is interpreted
  to be the `loose` param.
-- `includePrerelease`  Set to suppress the [default
+- `includePrerelease`: Set to suppress the [default
  behavior](https://github.com/npm/node-semver#prerelease-tags) of
  excluding prerelease tagged versions from ranges unless they are
  explicitly opted into.
@@ -415,16 +424,21 @@ Strict-mode Comparators and Ranges will be strict about the SemVer
strings that they parse.

* `valid(v)`: Return the parsed version, or null if it's not valid.
-* `inc(v, release)`: Return the version incremented by the release
-  type (`major`,   `premajor`, `minor`, `preminor`, `patch`,
-  `prepatch`, or `prerelease`), or null if it's not valid
+* `inc(v, releaseType, options, identifier, identifierBase)`: 
+  Return the version incremented by the release
+  type (`major`, `premajor`, `minor`, `preminor`, `patch`,
+  `prepatch`, `prerelease`, or `release`), or null if it's not valid
  * `premajor` in one call will bump the version up to the next major
    version and down to a prerelease of that major version.
    `preminor`, and `prepatch` work the same way.
-  * If called from a non-prerelease version, the `prerelease` will work the
-    same as `prepatch`. It increments the patch version, then makes a
+  * If called from a non-prerelease version, `prerelease` will work the
+    same as `prepatch`. It increments the patch version and ,then makes a
    prerelease. If the input version is already a prerelease it simply
    increments it.
+  * `release` will remove any prerelease part of the version.
+  * `identifier` can be used to prefix `premajor`, `preminor`,
+    `prepatch`, or `prerelease` version increments. `identifierBase`
+    is the base to be used for the `prerelease` identifier.
* `prerelease(v)`: Returns an array of prerelease components, or null
  if none exist. Example: `prerelease('1.2.3-alpha.1') -> ['alpha', 1]`
* `major(v)`: Return the major version number.
@@ -442,7 +456,7 @@ strings that they parse.
* `lt(v1, v2)`: `v1 < v2`
* `lte(v1, v2)`: `v1 <= v2`
* `eq(v1, v2)`: `v1 == v2` This is true if they're logically equivalent,
-  even if they're not the exact same string.  You already know how to
+  even if they're not the same string.  You already know how to
  compare strings.
* `neq(v1, v2)`: `v1 != v2` The opposite of `eq`.
* `cmp(v1, comparator, v2)`: Pass in a comparison string, and it'll call
@@ -451,41 +465,48 @@ strings that they parse.
  invalid comparison string is provided.
* `compare(v1, v2)`: Return `0` if `v1 == v2`, or `1` if `v1` is greater, or `-1` if
  `v2` is greater.  Sorts in ascending order if passed to `Array.sort()`.
-* `rcompare(v1, v2)`: The reverse of compare.  Sorts an array of versions
+* `rcompare(v1, v2)`: The reverse of `compare`.  Sorts an array of versions
  in descending order when passed to `Array.sort()`.
* `compareBuild(v1, v2)`: The same as `compare` but considers `build` when two versions
  are equal.  Sorts in ascending order if passed to `Array.sort()`.
-  `v2` is greater.  Sorts in ascending order if passed to `Array.sort()`.
-* `diff(v1, v2)`: Returns difference between two versions by the release type
+* `compareLoose(v1, v2)`: Short for `compare(v1, v2, { loose: true })`.
+* `diff(v1, v2)`: Returns the difference between two versions by the release type
  (`major`, `premajor`, `minor`, `preminor`, `patch`, `prepatch`, or `prerelease`),
  or null if the versions are the same.

+### Sorting
+
+* `sort(versions)`: Returns a sorted array of versions based on the `compareBuild` 
+  function.
+* `rsort(versions)`: The reverse of `sort`. Returns an array of versions based on
+  the `compareBuild` function in descending order.
+
### Comparators

* `intersects(comparator)`: Return true if the comparators intersect

### Ranges

-* `validRange(range)`: Return the valid range or null if it's not valid
+* `validRange(range)`: Return the valid range or null if it's not valid.
* `satisfies(version, range)`: Return true if the version satisfies the
  range.
* `maxSatisfying(versions, range)`: Return the highest version in the list
  that satisfies the range, or `null` if none of them do.
* `minSatisfying(versions, range)`: Return the lowest version in the list
  that satisfies the range, or `null` if none of them do.
-* `minVersion(range)`: Return the lowest version that can possibly match
+* `minVersion(range)`: Return the lowest version that can match
  the given range.
-* `gtr(version, range)`: Return `true` if version is greater than all the
+* `gtr(version, range)`: Return `true` if the version is greater than all the
  versions possible in the range.
-* `ltr(version, range)`: Return `true` if version is less than all the
+* `ltr(version, range)`: Return `true` if the version is less than all the
  versions possible in the range.
* `outside(version, range, hilo)`: Return true if the version is outside
  the bounds of the range in either the high or low direction.  The
  `hilo` argument must be either the string `'>'` or `'<'`.  (This is
  the function called by `gtr` and `ltr`.)
-* `intersects(range)`: Return true if any of the ranges comparators intersect
+* `intersects(range)`: Return true if any of the range comparators intersect.
* `simplifyRange(versions, range)`: Return a "simplified" range that
-  matches the same items in `versions` list as the range specified.  Note
+  matches the same items in the `versions` list as the range specified.  Note
  that it does *not* guarantee that it would match the same versions in all
  cases, only for the set of versions provided.  This is useful when
  generating ranges by joining together multiple versions with `||`
@@ -498,7 +519,7 @@ strings that they parse.
Note that, since ranges may be non-contiguous, a version might not be
greater than a range, less than a range, *or* satisfy a range!  For
example, the range `1.2 <1.2.9 || >2.0.0` would have a hole from `1.2.9`
-until `2.0.0`, so the version `1.2.10` would not be greater than the
+until `2.0.0`, so version `1.2.10` would not be greater than the
range (because `2.0.1` satisfies, which is higher), nor less than the
range (since `1.2.8` satisfies, which is lower), and it also does not
satisfy the range.
@@ -511,13 +532,13 @@ range, use the `satisfies(version, range)` function.
* `coerce(version, options)`: Coerces a string to semver if possible

This aims to provide a very forgiving translation of a non-semver string to
-semver. It looks for the first digit in a string, and consumes all
+semver. It looks for the first digit in a string and consumes all
remaining characters which satisfy at least a partial semver (e.g., `1`,
`1.2`, `1.2.3`) up to the max permitted length (256 characters).  Longer
versions are simply truncated (`4.6.3.9.2-alpha2` becomes `4.6.3`).  All
surrounding text is simply ignored (`v3.4 replaces v3.3.1` becomes
`3.4.0`).  Only text which lacks digits will fail coercion (`version one`
-is not valid).  The maximum  length for any semver component considered for
+is not valid).  The maximum length for any semver component considered for
coercion is 16 characters; longer components will be ignored
(`10000000000000000.4.7.4` becomes `4.7.4`).  The maximum value for any
semver component is `Number.MAX_SAFE_INTEGER || (2**53 - 1)`; higher value
@@ -529,6 +550,10 @@ tuple.  For example, `1.2.3.4` will return `2.3.4` in rtl mode, not
`4.0.0`.  `1.2.3/4` will return `4.0.0`, because the `4` is not a part of
any other overlapping SemVer tuple.

+If the `options.includePrerelease` flag is set, then the `coerce` result will contain
+prerelease and build parts of a version.  For example, `1.2.3.4-rc.1+rev.2`
+will preserve prerelease `rc.1` and build `rev.2` in the result.
+
### Clean

* `clean(version)`: Clean a string to be a valid semver if possible
@@ -543,7 +568,7 @@ ex.
* `s.clean(' = v 2.1.5-foo')`: `null`
* `s.clean(' = v 2.1.5-foo', { loose: true })`: `'2.1.5-foo'`
* `s.clean('=v2.1.5')`: `'2.1.5'`
-* `s.clean('  =v2.1.5')`: `2.1.5`
+* `s.clean('  =v2.1.5')`: `'2.1.5'`
* `s.clean('      2.1.5   ')`: `'2.1.5'`
* `s.clean('~1.0.0')`: `null`

@@ -589,7 +614,7 @@ eg), and then pull the module name into the documentation for that specific
thing.
-->

-You may pull in just the part of this semver utility that you need, if you
+You may pull in just the part of this semver utility that you need if you
are sensitive to packing and tree-shaking concerns.  The main
`require('semver')` export uses getter functions to lazily load the parts
of the API that are used.
@@ -632,6 +657,8 @@ The following modules are available:
* `require('semver/ranges/min-satisfying')`
* `require('semver/ranges/min-version')`
* `require('semver/ranges/outside')`
+* `require('semver/ranges/simplify')`
+* `require('semver/ranges/subset')`
* `require('semver/ranges/to-comparators')`
* `require('semver/ranges/valid')`

diff --git SECURITY.md SECURITY.md
index 9cd2deaf..4fe06a2a 100644
--- SECURITY.md
+++ SECURITY.md
@@ -2,7 +2,7 @@

GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub).

-If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 
+If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways.

If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [[email protected]](mailto:[email protected]).

diff --git a/benchmarks/bench-compare.js b/benchmarks/bench-compare.js
new file mode 100644
index 00000000..2dfc0900
--- /dev/null
+++ benchmarks/bench-compare.js
@@ -0,0 +1,48 @@
+const Benchmark = require('benchmark')
+const SemVer = require('../classes/semver')
+const suite = new Benchmark.Suite()
+
+const versions = ['1.0.3', '2.2.2', '2.3.0']
+const versionToCompare = '1.0.2'
+const option1 = { includePrelease: true }
+const option2 = { includePrelease: true, loose: true }
+const option3 = { includePrelease: true, loose: true, rtl: true }
+
+for (const version of versions) {
+  suite.add(`compare ${version} to ${versionToCompare}`, function () {
+    const semver = new SemVer(version)
+    semver.compare(versionToCompare)
+  })
+}
+
+for (const version of versions) {
+  suite.add(
+    `compare ${version} to ${versionToCompare} with option (${JSON.stringify(option1)})`,
+    function () {
+      const semver = new SemVer(version, option1)
+      semver.compare(versionToCompare)
+    })
+}
+
+for (const version of versions) {
+  suite.add(`compare ${version} to ${versionToCompare} with option (${JSON.stringify(option2)})`,
+    function () {
+      const semver = new SemVer(version, option2)
+      semver.compare(versionToCompare)
+    })
+}
+
+for (const version of versions) {
+  suite.add(
+    `compare ${version} to ${versionToCompare} with option (${JSON.stringify(option3)})`,
+    function () {
+      const semver = new SemVer(version, option3)
+      semver.compare(versionToCompare)
+    })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-diff.js b/benchmarks/bench-diff.js
new file mode 100644
index 00000000..97d74441
--- /dev/null
+++ benchmarks/bench-diff.js
@@ -0,0 +1,21 @@
+const Benchmark = require('benchmark')
+const diff = require('../functions/diff')
+const suite = new Benchmark.Suite()
+
+const cases = [
+  ['0.0.1', '0.0.1-pre', 'patch'],
+  ['0.0.1', '0.0.1-pre-2', 'patch'],
+  ['1.1.0', '1.1.0-pre', 'minor'],
+]
+
+for (const [v1, v2] of cases) {
+  suite.add(`diff(${v1}, ${v2})`, function () {
+    diff(v1, v2)
+  })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-parse-options.js b/benchmarks/bench-parse-options.js
new file mode 100644
index 00000000..41ab232c
--- /dev/null
+++ benchmarks/bench-parse-options.js
@@ -0,0 +1,33 @@
+const Benchmark = require('benchmark')
+const parseOptions = require('../internal/parse-options')
+const suite = new Benchmark.Suite()
+
+const options1 = {
+  includePrerelease: true,
+}
+
+const options2 = {
+  includePrerelease: true,
+  loose: true,
+}
+
+const options3 = {
+  includePrerelease: true,
+  loose: true,
+  rtl: false,
+}
+
+suite
+  .add('includePrerelease', function () {
+    parseOptions(options1)
+  })
+  .add('includePrerelease + loose', function () {
+    parseOptions(options2)
+  })
+  .add('includePrerelease + loose + rtl', function () {
+    parseOptions(options3)
+  })
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-parse.js b/benchmarks/bench-parse.js
new file mode 100644
index 00000000..e4180c44
--- /dev/null
+++ benchmarks/bench-parse.js
@@ -0,0 +1,25 @@
+const Benchmark = require('benchmark')
+const parse = require('../functions/parse')
+const { MAX_SAFE_INTEGER } = require('../internal/constants')
+const suite = new Benchmark.Suite()
+
+const cases = ['1.2.1', '1.2.2-4', '1.2.3-pre']
+const invalidCases = [`${MAX_SAFE_INTEGER}0.0.0`, 'hello, world', 'xyz']
+
+for (const test of cases) {
+  suite.add(`parse(${test})`, function () {
+    parse(test)
+  })
+}
+
+for (const test of invalidCases) {
+  suite.add(`invalid parse(${test})`, function () {
+    parse(test)
+  })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-satisfies.js b/benchmarks/bench-satisfies.js
new file mode 100644
index 00000000..a95a2c30
--- /dev/null
+++ benchmarks/bench-satisfies.js
@@ -0,0 +1,39 @@
+const Benchmark = require('benchmark')
+const satisfies = require('../functions/satisfies')
+const suite = new Benchmark.Suite()
+
+const versions = ['1.0.3||^2.0.0', '2.2.2||~3.0.0', '2.3.0||<4.0.0']
+const versionToCompare = '1.0.6'
+const option1 = { includePrelease: true }
+const option2 = { includePrelease: true, loose: true }
+const option3 = { includePrelease: true, loose: true, rtl: true }
+
+for (const version of versions) {
+  suite.add(`satisfies(${versionToCompare}, ${version})`, function () {
+    satisfies(versionToCompare, version)
+  })
+}
+
+for (const version of versions) {
+  suite.add(`satisfies(${versionToCompare}, ${version}, ${JSON.stringify(option1)})`, function () {
+    satisfies(versionToCompare, version, option1)
+  })
+}
+
+for (const version of versions) {
+  suite.add(`satisfies(${versionToCompare}, ${version}, ${JSON.stringify(option2)})`, function () {
+    satisfies(versionToCompare, version, option2)
+  })
+}
+
+for (const version of versions) {
+  suite.add(`satisfies(${versionToCompare}, ${version}, ${JSON.stringify(option3)})`, function () {
+    satisfies(versionToCompare, version, option3)
+  })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-subset.js b/benchmarks/bench-subset.js
new file mode 100644
index 00000000..f8825fae
--- /dev/null
+++ benchmarks/bench-subset.js
@@ -0,0 +1,25 @@
+const Benchmark = require('benchmark')
+const subset = require('../ranges/subset')
+const suite = new Benchmark.Suite()
+
+// taken from tests
+const cases = [
+  // everything is a subset of *
+  ['1.2.3', '*', true],
+  ['^1.2.3', '*', true],
+  ['^1.2.3-pre.0', '*', false],
+  ['^1.2.3-pre.0', '*', true, { includePrerelease: true }],
+  ['1 || 2 || 3', '*', true],
+]
+
+for (const [sub, dom] of cases) {
+  suite.add(`subset(${sub}, ${dom})`, function () {
+    subset(sub, dom)
+  })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git bin/semver.js bin/semver.js
index 242b7ade..22fc76ea 100755
--- bin/semver.js
+++ bin/semver.js
@@ -61,6 +61,7 @@ const main = () => {
        switch (argv[0]) {
          case 'major': case 'minor': case 'patch': case 'prerelease':
          case 'premajor': case 'preminor': case 'prepatch':
+          case 'release':
            inc = argv.shift()
            break
          default:
@@ -119,7 +120,11 @@ const main = () => {
      return fail()
    }
  }
-  return success(versions)
+  versions
+    .sort((a, b) => semver[reverse ? 'rcompare' : 'compare'](a, b, options))
+    .map(v => semver.clean(v, options))
+    .map(v => inc ? semver.inc(v, inc, options, identifier, identifierBase) : v)
+    .forEach(v => console.log(v))
}

const failInc = () => {
@@ -129,19 +134,6 @@ const failInc = () => {

const fail = () => process.exit(1)

-const success = () => {
-  const compare = reverse ? 'rcompare' : 'compare'
-  versions.sort((a, b) => {
-    return semver[compare](a, b, options)
-  }).map((v) => {
-    return semver.clean(v, options)
-  }).map((v) => {
-    return inc ? semver.inc(v, inc, options, identifier, identifierBase) : v
-  }).forEach((v, i, _) => {
-    console.log(v)
-  })
-}
-
const help = () => console.log(
`SemVer ${version}

@@ -158,7 +150,7 @@ Options:
-i --increment [<level>]
        Increment a version by the specified level.  Level can
        be one of: major, minor, patch, premajor, preminor,
-        prepatch, or prerelease.  Default level is 'patch'.
+        prepatch, prerelease, or release.  Default level is 'patch'.
        Only one version may be specified.

--preid <identifier>
diff --git classes/range.js classes/range.js
index a7d37203..ceee2314 100644
--- classes/range.js
+++ classes/range.js
@@ -1,3 +1,5 @@
+const SPACE_CHARACTERS = /\s+/g
+
// hoisted class for cyclic dependency
class Range {
  constructor (range, options) {
@@ -18,7 +20,7 @@ class Range {
      // just put it in the set and return
      this.raw = range.value
      this.set = [[range]]
-      this.format()
+      this.formatted = undefined
      return this
    }

@@ -29,16 +31,13 @@ class Range {
    // First reduce all whitespace as much as possible so we do not have to rely
    // on potentially slow regexes like \s*. This is then stored and used for
    // future error messages as well.
-    this.raw = range
-      .trim()
-      .split(/\s+/)
-      .join(' ')
+    this.raw = range.trim().replace(SPACE_CHARACTERS, ' ')

    // First, split on ||
    this.set = this.raw
      .split('||')
      // map the range to a 2d array of comparators
-      .map(r => this.parseRange(r))
+      .map(r => this.parseRange(r.trim()))
      // throw out any comparator lists that are empty
      // this generally means that it was not a valid range, which is allowed
      // in loose mode, but will still throw if the WHOLE range is invalid.
@@ -66,14 +65,29 @@ class Range {
      }
    }

-    this.format()
+    this.formatted = undefined
+  }
+
+  get range () {
+    if (this.formatted === undefined) {
+      this.formatted = ''
+      for (let i = 0; i < this.set.length; i++) {
+        if (i > 0) {
+          this.formatted += '||'
+        }
+        const comps = this.set[i]
+        for (let k = 0; k < comps.length; k++) {
+          if (k > 0) {
+            this.formatted += ' '
+          }
+          this.formatted += comps[k].toString().trim()
+        }
+      }
+    }
+    return this.formatted
  }

  format () {
-    this.range = this.set
-      .map((comps) => comps.join(' ').trim())
-      .join('||')
-      .trim()
    return this.range
  }

@@ -198,8 +212,8 @@ class Range {

module.exports = Range

-const LRU = require('lru-cache')
-const cache = new LRU({ max: 1000 })
+const LRU = require('../internal/lrucache')
+const cache = new LRU()

const parseOptions = require('../internal/parse-options')
const Comparator = require('./comparator')
@@ -470,9 +484,10 @@ const replaceGTE0 = (comp, options) => {
// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5
// 1.2.3 - 3.4 => >=1.2.0 <3.5.0-0 Any 3.4.x will do
// 1.2 - 3.4 => >=1.2.0 <3.5.0-0
+// TODO build?
const hyphenReplace = incPr => ($0,
  from, fM, fm, fp, fpr, fb,
-  to, tM, tm, tp, tpr, tb) => {
+  to, tM, tm, tp, tpr) => {
  if (isX(fM)) {
    from = ''
  } else if (isX(fm)) {
diff --git classes/semver.js classes/semver.js
index 84e84590..97049a40 100644
--- classes/semver.js
+++ classes/semver.js
@@ -10,7 +10,7 @@ class SemVer {

    if (version instanceof SemVer) {
      if (version.loose === !!options.loose &&
-          version.includePrerelease === !!options.includePrerelease) {
+        version.includePrerelease === !!options.includePrerelease) {
        return version
      } else {
        version = version.version
@@ -158,7 +158,7 @@ class SemVer {
    do {
      const a = this.build[i]
      const b = other.build[i]
-      debug('prerelease compare', i, a, b)
+      debug('build compare', i, a, b)
      if (a === undefined && b === undefined) {
        return 0
      } else if (b === undefined) {
@@ -176,6 +176,19 @@ class SemVer {
  // preminor will bump the version up to the next minor release, and immediately
  // down to pre-release. premajor and prepatch work the same way.
  inc (release, identifier, identifierBase) {
+    if (release.startsWith('pre')) {
+      if (!identifier && identifierBase === false) {
+        throw new Error('invalid increment argument: identifier is empty')
+      }
+      // Avoid an invalid semver results
+      if (identifier) {
+        const match = `-${identifier}`.match(this.options.loose ? re[t.PRERELEASELOOSE] : re[t.PRERELEASE])
+        if (!match || match[1] !== identifier) {
+          throw new Error(`invalid identifier: ${identifier}`)
+        }
+      }
+    }
+
    switch (release) {
      case 'premajor':
        this.prerelease.length = 0
@@ -206,6 +219,12 @@ class SemVer {
        }
        this.inc('pre', identifier, identifierBase)
        break
+      case 'release':
+        if (this.prerelease.length === 0) {
+          throw new Error(`version ${this.raw} is not a prerelease`)
+        }
+        this.prerelease.length = 0
+        break

      case 'major':
        // If this is a pre-major version, bump up to the same major version.
@@ -249,10 +268,6 @@ class SemVer {
      case 'pre': {
        const base = Number(identifierBase) ? 1 : 0

-        if (!identifier && identifierBase === false) {
-          throw new Error('invalid increment argument: identifier is empty')
-        }
-
        if (this.prerelease.length === 0) {
          this.prerelease = [base]
        } else {
diff --git functions/coerce.js functions/coerce.js
index febbff9c..b378dcea 100644
--- functions/coerce.js
+++ functions/coerce.js
@@ -19,34 +19,42 @@ const coerce = (version, options) => {

  let match = null
  if (!options.rtl) {
-    match = version.match(re[t.COERCE])
+    match = version.match(options.includePrerelease ? re[t.COERCEFULL] : re[t.COERCE])
  } else {
    // Find the right-most coercible string that does not share
    // a terminus with a more left-ward coercible string.
    // Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4'
+    // With includePrerelease option set, '1.2.3.4-rc' wants to coerce '2.3.4-rc', not '2.3.4'
    //
    // Walk through the string checking with a /g regexp
    // Manually set the index so as to pick up overlapping matches.
    // Stop when we get a match that ends at the string end, since no
    // coercible string can be more right-ward without the same terminus.
+    const coerceRtlRegex = options.includePrerelease ? re[t.COERCERTLFULL] : re[t.COERCERTL]
    let next
-    while ((next = re[t.COERCERTL].exec(version)) &&
+    while ((next = coerceRtlRegex.exec(version)) &&
        (!match || match.index + match[0].length !== version.length)
    ) {
      if (!match ||
            next.index + next[0].length !== match.index + match[0].length) {
        match = next
      }
-      re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length
+      coerceRtlRegex.lastIndex = next.index + next[1].length + next[2].length
    }
    // leave it in a clean state
-    re[t.COERCERTL].lastIndex = -1
+    coerceRtlRegex.lastIndex = -1
  }

  if (match === null) {
    return null
  }

-  return parse(`${match[2]}.${match[3] || '0'}.${match[4] || '0'}`, options)
+  const major = match[2]
+  const minor = match[3] || '0'
+  const patch = match[4] || '0'
+  const prerelease = options.includePrerelease && match[5] ? `-${match[5]}` : ''
+  const build = options.includePrerelease && match[6] ? `+${match[6]}` : ''
+
+  return parse(`${major}.${minor}.${patch}${prerelease}${build}`, options)
}
module.exports = coerce
diff --git functions/diff.js functions/diff.js
index fc224e30..33171dc1 100644
--- functions/diff.js
+++ functions/diff.js
@@ -27,20 +27,13 @@ const diff = (version1, version2) => {
      return 'major'
    }

-    // Otherwise it can be determined by checking the high version
-
-    if (highVersion.patch) {
-      // anything higher than a patch bump would result in the wrong version
+    // If the main part has no difference
+    if (lowVersion.compareMain(highVersion) === 0) {
+      if (lowVersion.minor && !lowVersion.patch) {
+        return 'minor'
+      }
      return 'patch'
    }
-
-    if (highVersion.minor) {
-      // anything higher than a minor bump would result in the wrong version
-      return 'minor'
-    }
-
-    // bumping major/minor/patch all have same result
-    return 'major'
  }

  // add the `pre` prefix if we are going to a prerelease version
diff --git a/internal/lrucache.js b/internal/lrucache.js
new file mode 100644
index 00000000..6d89ec94
--- /dev/null
+++ internal/lrucache.js
@@ -0,0 +1,40 @@
+class LRUCache {
+  constructor () {
+    this.max = 1000
+    this.map = new Map()
+  }
+
+  get (key) {
+    const value = this.map.get(key)
+    if (value === undefined) {
+      return undefined
+    } else {
+      // Remove the key from the map and add it to the end
+      this.map.delete(key)
+      this.map.set(key, value)
+      return value
+    }
+  }
+
+  delete (key) {
+    return this.map.delete(key)
+  }
+
+  set (key, value) {
+    const deleted = this.delete(key)
+
+    if (!deleted && value !== undefined) {
+      // If cache is full, delete the least recently used item
+      if (this.map.size >= this.max) {
+        const firstKey = this.map.keys().next().value
+        this.delete(firstKey)
+      }
+
+      this.map.set(key, value)
+    }
+
+    return this
+  }
+}
+
+module.exports = LRUCache
diff --git internal/re.js internal/re.js
index 9f5e36d5..fd8920e7 100644
--- internal/re.js
+++ internal/re.js
@@ -1,4 +1,8 @@
-const { MAX_SAFE_COMPONENT_LENGTH, MAX_SAFE_BUILD_LENGTH } = require('./constants')
+const {
+  MAX_SAFE_COMPONENT_LENGTH,
+  MAX_SAFE_BUILD_LENGTH,
+  MAX_LENGTH,
+} = require('./constants')
const debug = require('./debug')
exports = module.exports = {}

@@ -19,7 +23,7 @@ const LETTERDASHNUMBER = '[a-zA-Z0-9-]'
// all input should have extra whitespace removed.
const safeRegexReplacements = [
  ['\\s', 1],
-  ['\\d', MAX_SAFE_COMPONENT_LENGTH],
+  ['\\d', MAX_LENGTH],
  [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH],
]

@@ -150,12 +154,17 @@ createToken('XRANGELOOSE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`)

// Coercion.
// Extract anything that could conceivably be a part of a valid semver
-createToken('COERCE', `${'(^|[^\\d])' +
+createToken('COERCEPLAIN', `${'(^|[^\\d])' +
              '(\\d{1,'}${MAX_SAFE_COMPONENT_LENGTH}})` +
              `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` +
-              `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` +
+              `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?`)
+createToken('COERCE', `${src[t.COERCEPLAIN]}(?:$|[^\\d])`)
+createToken('COERCEFULL', src[t.COERCEPLAIN] +
+              `(?:${src[t.PRERELEASE]})?` +
+              `(?:${src[t.BUILD]})?` +
              `(?:$|[^\\d])`)
createToken('COERCERTL', src[t.COERCE], true)
+createToken('COERCERTLFULL', src[t.COERCEFULL], true)

// Tilde ranges.
// Meaning is "reasonably at or greater than"
diff --git package.json package.json
index 378164a7..405fb66e 100644
--- package.json
+++ package.json
@@ -1,26 +1,28 @@
{
  "name": "semver",
-  "version": "7.5.3",
+  "version": "7.7.0",
  "description": "The semantic version parser used by npm.",
  "main": "index.js",
  "scripts": {
    "test": "tap",
    "snap": "tap",
-    "lint": "eslint \"**/*.js\"",
+    "lint": "npm run eslint",
    "postlint": "template-oss-check",
-    "lintfix": "npm run lint -- --fix",
+    "lintfix": "npm run eslint -- --fix",
    "posttest": "npm run lint",
-    "template-oss-apply": "template-oss-apply --force"
+    "template-oss-apply": "template-oss-apply --force",
+    "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\""
  },
  "devDependencies": {
-    "@npmcli/eslint-config": "^4.0.0",
-    "@npmcli/template-oss": "4.15.1",
+    "@npmcli/eslint-config": "^5.0.0",
+    "@npmcli/template-oss": "4.23.4",
+    "benchmark": "^2.1.4",
    "tap": "^16.0.0"
  },
  "license": "ISC",
  "repository": {
    "type": "git",
-    "url": "https://github.com/npm/node-semver.git"
+    "url": "git+https://github.com/npm/node-semver.git"
  },
  "bin": {
    "semver": "bin/semver.js"
@@ -47,23 +49,11 @@
  "engines": {
    "node": ">=10"
  },
-  "dependencies": {
-    "lru-cache": "^6.0.0"
-  },
  "author": "GitHub Inc.",
  "templateOSS": {
    "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
-    "version": "4.15.1",
+    "version": "4.23.4",
    "engines": ">=10",
-    "ciVersions": [
-      "10.0.0",
-      "10.x",
-      "12.x",
-      "14.x",
-      "16.x",
-      "18.x"
-    ],
-    "npmSpec": "8",
    "distPaths": [
      "classes/",
      "functions/",
@@ -80,7 +70,8 @@
      "/ranges/",
      "/index.js",
      "/preload.js",
-      "/range.bnf"
+      "/range.bnf",
+      "/benchmarks"
    ],
    "publish": "true"
  }
diff --git release-please-config.json release-please-config.json
index 73d1e353..a1676b9c 100644
--- release-please-config.json
+++ release-please-config.json
@@ -1,5 +1,4 @@
{
-  "exclude-packages-from-root": true,
  "group-pull-request-title-pattern": "chore: release ${version}",
  "pull-request-title-pattern": "chore: release${component} ${version}",
  "changelog-sections": [
@@ -25,6 +24,7 @@
    },
    {
      "type": "chore",
+      "section": "Chores",
      "hidden": true
    }
  ],
@@ -32,5 +32,6 @@
    ".": {
      "package-name": ""
    }
-  }
+  },
+  "prerelease-type": "pre"
}
diff --git tap-snapshots/test/bin/semver.js.test.cjs tap-snapshots/test/bin/semver.js.test.cjs
index e820ca47..4938f10d 100644
--- tap-snapshots/test/bin/semver.js.test.cjs
+++ tap-snapshots/test/bin/semver.js.test.cjs
@@ -70,7 +70,7 @@ Object {
    -i --increment [<level>]
            Increment a version by the specified level.  Level can
            be one of: major, minor, patch, premajor, preminor,
-            prepatch, or prerelease.  Default level is 'patch'.
+            prepatch, prerelease, or release.  Default level is 'patch'.
            Only one version may be specified.
    
    --preid <identifier>
@@ -131,7 +131,7 @@ Object {
    -i --increment [<level>]
            Increment a version by the specified level.  Level can
            be one of: major, minor, patch, premajor, preminor,
-            prepatch, or prerelease.  Default level is 'patch'.
+            prepatch, prerelease, or release.  Default level is 'patch'.
            Only one version may be specified.
    
    --preid <identifier>
@@ -192,7 +192,7 @@ Object {
    -i --increment [<level>]
            Increment a version by the specified level.  Level can
            be one of: major, minor, patch, premajor, preminor,
-            prepatch, or prerelease.  Default level is 'patch'.
+            prepatch, prerelease, or release.  Default level is 'patch'.
            Only one version may be specified.
    
    --preid <identifier>
@@ -253,7 +253,7 @@ Object {
    -i --increment [<level>]
            Increment a version by the specified level.  Level can
            be one of: major, minor, patch, premajor, preminor,
-            prepatch, or prerelease.  Default level is 'patch'.
+            prepatch, prerelease, or release.  Default level is 'patch'.
            Only one version may be specified.
    
    --preid <identifier>
@@ -348,6 +348,15 @@ Object {
}
`

+exports[`test/bin/semver.js TAP inc tests > -i release 1.0.0-pre`] = `
+Object {
+  "code": 0,
+  "err": "",
+  "out": "1.0.0\\n",
+  "signal": null,
+}
+`
+
exports[`test/bin/semver.js TAP sorting and filtering > 1.2.3 -v 3.2.1 --version 2.3.4 -rv 1`] = `
Object {
  "code": 0,
diff --git test/classes/range.js test/classes/range.js
index 24686adf..c1e6eb1b 100644
--- test/classes/range.js
+++ test/classes/range.js
@@ -82,6 +82,15 @@ test('tostrings', (t) => {
  t.end()
})

+test('formatted value is calculated lazily and cached', (t) => {
+  const r = new Range('>= v1.2.3')
+  t.equal(r.formatted, undefined)
+  t.equal(r.format(), '>=1.2.3')
+  t.equal(r.formatted, '>=1.2.3')
+  t.equal(r.format(), '>=1.2.3')
+  t.end()
+})
+
test('ranges intersect', (t) => {
  rangeIntersection.forEach(([r0, r1, expect]) => {
    t.test(`${r0} <~> ${r1}`, t => {
@@ -105,3 +114,13 @@ test('missing range parameter in range intersect', (t) => {
  'throws type error')
  t.end()
})
+
+test('cache', (t) => {
+  const cached = Symbol('cached')
+  const r1 = new Range('1.0.0')
+  r1.set[0][cached] = true
+  const r2 = new Range('1.0.0')
+  t.equal(r1.set[0][cached], true)
+  t.equal(r2.set[0][cached], true) // Will be true, showing it's cached.
+  t.end()
+})
diff --git test/classes/semver.js test/classes/semver.js
index 1e4d48f8..946fa417 100644
--- test/classes/semver.js
+++ test/classes/semver.js
@@ -106,6 +106,34 @@ test('incrementing', t => {
  }))
})

+test('invalid increments', (t) => {
+  t.throws(
+    () => new SemVer('1.2.3').inc('prerelease', '', false),
+    Error('invalid increment argument: identifier is empty')
+  )
+  t.throws(
+    () => new SemVer('1.2.3-dev').inc('prerelease', 'dev', false),
+    Error('invalid increment argument: identifier already exists')
+  )
+  t.throws(
+    () => new SemVer('1.2.3').inc('prerelease', 'invalid/preid'),
+    Error('invalid identifier: invalid/preid')
+  )
+
+  t.end()
+})
+
+test('increment side-effects', (t) => {
+  const v = new SemVer('1.0.0')
+  try {
+    v.inc('prerelease', 'hot/mess')
+  } catch (er) {
+    // ignore but check that the version has not changed
+  }
+  t.equal(v.toString(), '1.0.0')
+  t.end()
+})
+
test('compare main vs pre', (t) => {
  const s = new SemVer('1.2.3')
  t.equal(s.compareMain('2.3.4'), -1)
@@ -123,25 +151,6 @@ test('compare main vs pre', (t) => {
  t.end()
})

-test('invalid version numbers', (t) => {
-  ['1.2.3.4', 'NOT VALID', 1.2, null, 'Infinity.NaN.Infinity'].forEach((v) => {
-    t.throws(
-      () => {
-        new SemVer(v) // eslint-disable-line no-new
-      },
-      {
-        name: 'TypeError',
-        message:
-          typeof v === 'string'
-            ? `Invalid Version: ${v}`
-            : `Invalid version. Must be a string. Got type "${typeof v}".`,
-      }
-    )
-  })
-
-  t.end()
-})
-
test('compareBuild', (t) => {
  const noBuild = new SemVer('1.0.0')
  const build0 = new SemVer('1.0.0+0')
diff --git test/fixtures/increments.js test/fixtures/increments.js
index 65e9530b..a9b06358 100644
--- test/fixtures/increments.js
+++ test/fixtures/increments.js
@@ -40,7 +40,12 @@ module.exports = [
  ['1.2.3-1', 'premajor', '2.0.0-0'],
  ['1.2.0-1', 'minor', '1.2.0'],
  ['1.0.0-1', 'major', '1.0.0'],
+  ['1.0.0-1', 'release', '1.0.0'],
+  ['1.2.0-1', 'release', '1.2.0'],
+  ['1.2.3-1', 'release', '1.2.3'],
+  ['1.2.3', 'release', null],

+  // [version, inc, result, identifierIndex, loose, identifier]
  ['1.2.3', 'major', '2.0.0', false, 'dev'],
  ['1.2.3', 'minor', '1.3.0', false, 'dev'],
  ['1.2.3', 'patch', '1.2.4', false, 'dev'],
@@ -88,7 +93,7 @@ module.exports = [
  ['1.2.3-1.1', 'prerelease', '1.2.3-1.2', false, '1'],
  ['1.2.3-1.1', 'prerelease', '1.2.3-2.0', false, '2'],

-  // [version, inc, result, identifierIndex, loose, identifier]
+  // [version, inc, result, identifierIndex, loose, identifier, identifierBase]
  ['1.2.0-1', 'prerelease', '1.2.0-alpha.0', false, 'alpha', '0'],
  ['1.2.1', 'prerelease', '1.2.2-alpha.0', false, 'alpha', '0'],
  ['0.2.0', 'prerelease', '0.2.1-alpha.0', false, 'alpha', '0'],
@@ -124,4 +129,7 @@ module.exports = [
  ['1.2.0-dev', 'prepatch', '1.2.1-dev', false, 'dev', false],
  ['1.2.0', 'prerelease', null, false, '', false],
  ['1.0.0-rc.1+build.4', 'prerelease', '1.0.0-rc.2', 'rc', false],
+  ['1.2.0', 'prerelease', null, false, 'invalid/preid'],
+  ['1.2.0', 'prerelease', null, false, 'invalid+build'],
+  ['1.2.0beta', 'prerelease', null, { loose: true }, 'invalid/preid'],
]
diff --git test/fixtures/range-exclude.js test/fixtures/range-exclude.js
index 4b6c5631..2789148a 100644
--- test/fixtures/range-exclude.js
+++ test/fixtures/range-exclude.js
@@ -102,4 +102,6 @@ module.exports = [
  ['>=1.0.0 <1.1.0', '1.1.0', { includePrerelease: true }],
  ['>=1.0.0 <1.1.0', '1.1.0-pre'],
  ['>=1.0.0 <1.1.0-pre', '1.1.0-pre'],
+
+  ['== 1.0.0 || foo', '2.0.0', { loose: true }],
]
diff --git test/functions/clean.js test/functions/clean.js
index 1df155bf..830e824b 100644
--- test/functions/clean.js
+++ test/functions/clean.js
@@ -17,6 +17,7 @@ test('clean tests', (t) => {
    ['~1.2.3', null],
    ['<=1.2.3', null],
    ['1.2.x', null],
+    ['0.12.0-dev.1150+3c22cecee', '0.12.0-dev.1150'],
  ].forEach(([range, version]) => {
    const msg = `clean(${range}) = ${version}`
    t.equal(clean(range), version, msg)
diff --git test/functions/coerce.js test/functions/coerce.js
index ad9f199f..24e2ff76 100644
--- test/functions/coerce.js
+++ test/functions/coerce.js
@@ -110,13 +110,47 @@ test('coerce tests', (t) => {
    ['1.2.3/6', '6.0.0', { rtl: true }],
    ['1.2.3.4', '2.3.4', { rtl: true }],
    ['1.2.3.4xyz', '2.3.4', { rtl: true }],
+
+    ['1-rc.5', '1.0.0-rc.5', { includePrerelease: true }],
+    ['1.2-rc.5', '1.2.0-rc.5', { includePrerelease: true }],
+    ['1.2.3-rc.5', '1.2.3-rc.5', { includePrerelease: true }],
+    ['1.2.3-rc.5/a', '1.2.3-rc.5', { includePrerelease: true }],
+    ['1.2.3.4-rc.5', '1.2.3', { includePrerelease: true }],
+    ['1.2.3.4+rev.6', '1.2.3', { includePrerelease: true }],
+
+    ['1+rev.6', '1.0.0+rev.6', { includePrerelease: true }],
+    ['1.2+rev.6', '1.2.0+rev.6', { includePrerelease: true }],
+    ['1.2.3+rev.6', '1.2.3+rev.6', { includePrerelease: true }],
+    ['1.2.3+rev.6/a', '1.2.3+rev.6', { includePrerelease: true }],
+    ['1.2.3.4-rc.5', '1.2.3', { includePrerelease: true }],
+    ['1.2.3.4+rev.6', '1.2.3', { includePrerelease: true }],
+
+    ['1-rc.5+rev.6', '1.0.0-rc.5+rev.6', { includePrerelease: true }],
+    ['1.2-rc.5+rev.6', '1.2.0-rc.5+rev.6', { includePrerelease: true }],
+    ['1.2.3-rc.5+rev.6', '1.2.3-rc.5+rev.6', { includePrerelease: true }],
+    ['1.2.3-rc.5+rev.6/a', '1.2.3-rc.5+rev.6', { includePrerelease: true }],
+
+    ['1.2-rc.5+rev.6', '1.2.0-rc.5+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3-rc.5+rev.6', '1.2.3-rc.5+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4-rc.5+rev.6', '2.3.4-rc.5+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4-rc.5', '2.3.4-rc.5', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4+rev.6', '2.3.4+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4-rc.5+rev.6/7', '7.0.0', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4-rc/7.5+rev.6', '7.5.0+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4/7-rc.5+rev.6', '7.0.0-rc.5+rev.6', { rtl: true, includePrerelease: true }],
  ]
  coerceToValid.forEach(([input, expected, options]) => {
-    const msg = `coerce(${input}) should become ${expected}`
-    t.same((coerce(input, options) || {}).version, expected, msg)
+    const coerceExpression = `coerce(${input}, ${JSON.stringify(options)})`
+    const coercedVersion = coerce(input, options) || {}
+    const expectedVersion = parse(expected)
+    t.equal(expectedVersion.compare(coercedVersion), 0,
+      `${coerceExpression} should be equal to ${expectedVersion}`)
+    t.equal(expectedVersion.compareBuild(coercedVersion), 0,
+      `${coerceExpression} build should be equal to ${expectedVersion}`)
  })

  t.same(valid(coerce('42.6.7.9.3-alpha')), '42.6.7')
+  t.same(valid(coerce('42.6.7-alpha+rev.1', { includePrerelease: true })), '42.6.7-alpha')
  t.same(valid(coerce('v2')), '2.0.0')

  t.end()
diff --git test/functions/diff.js test/functions/diff.js
index 720e159b..80f5e3c3 100644
--- test/functions/diff.js
+++ test/functions/diff.js
@@ -34,6 +34,13 @@ test('diff versions test', (t) => {
    ['1.0.0-1', '2.0.0-1', 'premajor'],
    ['1.0.0-1', '1.1.0-1', 'preminor'],
    ['1.0.0-1', '1.0.1-1', 'prepatch'],
+    ['1.7.2-1', '1.8.1', 'minor'],
+    ['1.1.1-pre', '2.1.1-pre', 'premajor'],
+    ['1.1.1-pre', '2.1.1', 'major'],
+    ['1.2.3-1', '1.2.3', 'patch'],
+    ['1.4.0-1', '2.3.5', 'major'],
+    ['1.6.1-5', '1.7.2', 'minor'],
+    ['2.0.0-1', '2.1.1', 'major'],
  ].forEach((v) => {
    const version1 = v[0]
    const version2 = v[1]
diff --git test/functions/valid.js test/functions/valid.js
index ab51fed3..33399ed7 100644
--- test/functions/valid.js
+++ test/functions/valid.js
@@ -2,6 +2,7 @@ const t = require('tap')
const valid = require('../../functions/valid')
const SemVer = require('../../classes/semver')
const invalidVersions = require('../fixtures/invalid-versions')
+const { MAX_SAFE_INTEGER } = require('../../internal/constants')

t.test('returns null instead of throwing when presented with garbage', t => {
  t.plan(invalidVersions.length)
@@ -17,3 +18,12 @@ t.test('validate a version into a SemVer object', t => {
  t.equal(valid('4.2.0foo', { loose: true }), '4.2.0-foo', 'looseness as an option')
  t.end()
})
+
+t.test('long build id', t => {
+  const longBuild = '-928490632884417731e7af463c92b034d6a78268fc993bcb88a57944'
+  const shortVersion = '1.1.1'
+  const longVersion = `${MAX_SAFE_INTEGER}.${MAX_SAFE_INTEGER}.${MAX_SAFE_INTEGER}`
+  t.equal(valid(shortVersion + longBuild), shortVersion + longBuild)
+  t.equal(valid(longVersion + longBuild), longVersion + longBuild)
+  t.end()
+})
diff --git test/integration/whitespace.js test/integration/whitespace.js
index ae1451b1..a3541325 100644
--- test/integration/whitespace.js
+++ test/integration/whitespace.js
@@ -29,8 +29,8 @@ test('range with 0', (t) => {
  t.throws(() => new Range(r).range)
  t.equal(validRange(r), null)
  t.throws(() => minVersion(r).version)
-  t.equal(minSatisfying(['1.2.3']), null)
-  t.equal(maxSatisfying(['1.2.3']), null)
+  t.equal(minSatisfying(['1.2.3'], r), null)
+  t.equal(maxSatisfying(['1.2.3'], r), null)
  t.end()
})

diff --git a/test/internal/lrucache.js b/test/internal/lrucache.js
new file mode 100644
index 00000000..83a0e797
--- /dev/null
+++ test/internal/lrucache.js
@@ -0,0 +1,19 @@
+const { test } = require('tap')
+const LRUCache = require('../../internal/lrucache')
+
+test('basic cache operation', t => {
+  const c = new LRUCache()
+  const max = 1000
+
+  for (let i = 0; i < max; i++) {
+    t.equal(c.set(i, i), c)
+  }
+  for (let i = 0; i < max; i++) {
+    t.equal(c.get(i), i)
+  }
+  c.set(1001, 1001)
+  // lru item should be gone
+  t.equal(c.get(0), undefined)
+  c.set(42, undefined)
+  t.end()
+})


Description

This PR introduces several significant updates to the node-semver library, including new features, performance improvements, and dependency changes. The main changes include adding a "release" increment type, optimizing the Range parsing logic, and replacing the third-party LRU cache with an internal implementation.

Changes

Changes

By file category:

Core functionality:

  • classes/range.js:

    • Optimized Range parsing and formatting
    • Added lazy formatting of ranges
    • Improved whitespace handling
  • classes/semver.js:

    • Added 'release' increment type
    • Added identifier validation in inc()
    • Improved error handling
  • functions/coerce.js:

    • Added support for preserving pre-release and build parts
    • Enhanced RTL coercion logic

Performance:

  • Added new benchmarking suite in /benchmarks
  • Replaced external lru-cache dependency with internal implementation
  • Optimized various parsing operations

Infrastructure:

  • Updated CI configuration for newer Node.js versions
  • Added new GitHub Actions workflows
  • Updated dependabot configuration
  • Improved branch protection rules

Documentation:

  • Enhanced README.md with clearer examples and better formatting
  • Added new documentation for the 'release' increment type
  • Improved API documentation accuracy
sequenceDiagram
    participant App as Application
    participant Semver as SemVer Class
    participant Range as Range Class
    participant Cache as Internal LRU Cache
    
    App->>Semver: Parse Version
    Semver->>Cache: Check Cache
    alt Cache Hit
        Cache-->>Semver: Return Cached Result
    else Cache Miss
        Semver->>Semver: Parse Version
        Semver->>Cache: Store Result
    end
    Semver-->>App: Return Version Object
    
    App->>Range: Parse Range
    Range->>Cache: Check Cache
    alt Cache Hit
        Cache-->>Range: Return Cached Result
    else Cache Miss
        Range->>Range: Parse Range
        Range->>Cache: Store Result
    end
    Range-->>App: Return Range Object
Loading

Possible Issues

  1. The removal of the lru-cache dependency could potentially impact applications that rely on specific cache behaviors of the previous implementation
  2. The changes to Range parsing might affect applications that depend on the exact formatting of range strings

Security Hotspots

None identified - the changes are primarily focused on internal logic improvements and don't introduce new attack vectors.

@renovate renovate bot force-pushed the renovate/semver-7.x branch from 165e93d to 07a80d1 Compare February 11, 2025 12:48
@renovate renovate bot changed the title chore(deps): update dependency semver to v7.7.0 chore(deps): update dependency semver to v7.7.1 Feb 11, 2025
Copy link

[puLL-Merge] - npm/[email protected]

Diff
diff --git .commitlintrc.js .commitlintrc.js
index 5b0b1a52..b706e527 100644
--- .commitlintrc.js
+++ .commitlintrc.js
@@ -5,6 +5,8 @@ module.exports = {
   rules: {
     'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']],
     'header-max-length': [2, 'always', 80],
-    'subject-case': [0, 'always', ['lower-case', 'sentence-case', 'start-case']],
+    'subject-case': [0],
+    'body-max-line-length': [0],
+    'footer-max-line-length': [0],
   },
 }
diff --git .eslintrc.js .eslintrc.js
index 5db9f815..f21d26ec 100644
--- .eslintrc.js
+++ .eslintrc.js
@@ -10,6 +10,9 @@ const localConfigs = readdir(__dirname)
 
 module.exports = {
   root: true,
+  ignorePatterns: [
+    'tap-testdir*/',
+  ],
   extends: [
     '@npmcli',
     ...localConfigs,
diff --git a/.github/actions/create-check/action.yml b/.github/actions/create-check/action.yml
new file mode 100644
index 00000000..d1220c90
--- /dev/null
+++ .github/actions/create-check/action.yml
@@ -0,0 +1,52 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: 'Create Check'
+inputs:
+  name:
+    required: true
+  token:
+    required: true
+  sha:
+    required: true
+  check-name:
+    default: ''
+outputs:
+  check-id:
+    value: ${{ steps.create-check.outputs.check_id }}
+runs:
+  using: "composite"
+  steps:
+    - name: Get Workflow Job
+      uses: actions/github-script@v7
+      id: workflow
+      env:
+        JOB_NAME: "${{ inputs.name }}"
+        SHA: "${{ inputs.sha }}"
+      with:
+        result-encoding: string
+        script: |
+          const { repo: { owner, repo}, runId, serverUrl } = context
+          const { JOB_NAME, SHA } = process.env
+
+          const job = await github.rest.actions.listJobsForWorkflowRun({
+            owner,
+            repo,
+            run_id: runId,
+            per_page: 100
+          }).then(r => r.data.jobs.find(j => j.name.endsWith(JOB_NAME)))
+
+          return [
+            `This check is assosciated with ${serverUrl}/${owner}/${repo}/commit/${SHA}.`,
+            'Run logs:',
+            job?.html_url || `could not be found for a job ending with: "${JOB_NAME}"`,
+          ].join(' ')
+    - name: Create Check
+      uses: LouisBrunner/[email protected]
+      id: create-check
+      with:
+        token: ${{ inputs.token }}
+        sha: ${{ inputs.sha }}
+        status: in_progress
+        name: ${{ inputs.check-name || inputs.name }}
+        output: |
+          {"summary":"${{ steps.workflow.outputs.result }}"}
diff --git a/.github/actions/install-latest-npm/action.yml b/.github/actions/install-latest-npm/action.yml
new file mode 100644
index 00000000..580603dd
--- /dev/null
+++ .github/actions/install-latest-npm/action.yml
@@ -0,0 +1,58 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: 'Install Latest npm'
+description: 'Install the latest version of npm compatible with the Node version'
+inputs:
+  node:
+    description: 'Current Node version'
+    required: true
+runs:
+  using: "composite"
+  steps:
+    # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows
+    - name: Update Windows npm
+      if: |
+        runner.os == 'Windows' && (
+          startsWith(inputs.node, 'v10.') ||
+          startsWith(inputs.node, 'v12.') ||
+          startsWith(inputs.node, 'v14.')
+        )
+      shell: cmd
+      run: |
+        curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
+        tar xf npm-7.5.4.tgz
+        cd package
+        node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
+        cd ..
+        rmdir /s /q package
+    - name: Install Latest npm
+      shell: bash
+      env:
+        NODE_VERSION: ${{ inputs.node }}
+      working-directory: ${{ runner.temp }}
+      run: |
+        MATCH=""
+        SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6")
+
+        echo "node@$NODE_VERSION"
+
+        for SPEC in ${SPECS[@]}; do
+          ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node')
+          echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)"
+
+          if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then
+            MATCH=$SPEC
+            echo "Found compatible version: npm@$MATCH"
+            break
+          fi
+        done
+
+        if [ -z $MATCH ]; then
+          echo "Could not find a compatible version of npm for node@$NODE_VERSION"
+          exit 1
+        fi
+
+        npm i --prefer-online --no-fund --no-audit -g npm@$MATCH
+    - name: npm Version
+      shell: bash
+      run: npm -v
diff --git .github/dependabot.yml .github/dependabot.yml
index 8da2a452..d735ccf2 100644
--- .github/dependabot.yml
+++ .github/dependabot.yml
@@ -7,6 +7,7 @@ updates:
     directory: /
     schedule:
       interval: daily
+    target-branch: "main"
     allow:
       - dependency-type: direct
     versioning-strategy: increase-if-necessary
@@ -15,3 +16,38 @@ updates:
       prefix-development: chore
     labels:
       - "Dependencies"
+    open-pull-requests-limit: 10
+  - package-ecosystem: npm
+    directory: /
+    schedule:
+      interval: daily
+    target-branch: "release/v5"
+    allow:
+      - dependency-type: direct
+        dependency-name: "@npmcli/template-oss"
+    versioning-strategy: increase-if-necessary
+    commit-message:
+      prefix: deps
+      prefix-development: chore
+    labels:
+      - "Dependencies"
+      - "Backport"
+      - "release/v5"
+    open-pull-requests-limit: 10
+  - package-ecosystem: npm
+    directory: /
+    schedule:
+      interval: daily
+    target-branch: "release/v6"
+    allow:
+      - dependency-type: direct
+        dependency-name: "@npmcli/template-oss"
+    versioning-strategy: increase-if-necessary
+    commit-message:
+      prefix: deps
+      prefix-development: chore
+    labels:
+      - "Dependencies"
+      - "Backport"
+      - "release/v6"
+    open-pull-requests-limit: 10
diff --git .github/settings.yml .github/settings.yml
index 107aa0ad..206b6eeb 100644
--- .github/settings.yml
+++ .github/settings.yml
@@ -15,6 +15,35 @@ branches:
     protection:
       required_status_checks: null
       enforce_admins: true
+      block_creations: true
+      required_pull_request_reviews:
+        required_approving_review_count: 1
+        require_code_owner_reviews: true
+        require_last_push_approval: true
+        dismiss_stale_reviews: true
+      restrictions:
+        apps: []
+        users: []
+        teams: [ "cli-team" ]
+  - name: release/v5
+    protection:
+      required_status_checks: null
+      enforce_admins: true
+      block_creations: true
+      required_pull_request_reviews:
+        required_approving_review_count: 1
+        require_code_owner_reviews: true
+        require_last_push_approval: true
+        dismiss_stale_reviews: true
+      restrictions:
+        apps: []
+        users: []
+        teams: [ "cli-team" ]
+  - name: release/v6
+    protection:
+      required_status_checks: null
+      enforce_admins: true
+      block_creations: true
       required_pull_request_reviews:
         required_approving_review_count: 1
         require_code_owner_reviews: true
diff --git .github/workflows/audit.yml .github/workflows/audit.yml
index 8b8f3748..a3ae7257 100644
--- .github/workflows/audit.yml
+++ .github/workflows/audit.yml
@@ -18,19 +18,21 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund --package-lock
       - name: Run Production Audit
diff --git .github/workflows/ci-release.yml .github/workflows/ci-release.yml
index 98b70866..f6ab948b 100644
--- .github/workflows/ci-release.yml
+++ .github/workflows/ci-release.yml
@@ -27,65 +27,32 @@ jobs:
       run:
         shell: bash
     steps:
-      - name: Get Workflow Job
-        uses: actions/github-script@v6
-        if: inputs.check-sha
-        id: check-output
-        env:
-          JOB_NAME: "Lint All"
-          MATRIX_NAME: ""
-        with:
-          script: |
-            const { owner, repo } = context.repo
-
-            const { data } = await github.rest.actions.listJobsForWorkflowRun({
-              owner,
-              repo,
-              run_id: context.runId,
-              per_page: 100
-            })
-
-            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
-            const job = data.jobs.find(j => j.name.endsWith(jobName))
-            const jobUrl = job?.html_url
-
-            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
-
-            let summary = `This check is assosciated with ${shaUrl}\n\n`
-
-            if (jobUrl) {
-              summary += `For run logs, click here: ${jobUrl}`
-            } else {
-              summary += `Run logs could not be found for a job with name: "${jobName}"`
-            }
-
-            return { summary }
-      - name: Create Check
-        uses: LouisBrunner/[email protected]
-        id: check
-        if: inputs.check-sha
-        with:
-          token: ${{ secrets.GITHUB_TOKEN }}
-          status: in_progress
-          name: Lint All
-          sha: ${{ inputs.check-sha }}
-          output: ${{ steps.check-output.outputs.result }}
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           ref: ${{ inputs.ref }}
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
+      - name: Create Check
+        id: create-check
+        if: ${{ inputs.check-sha }}
+        uses: ./.github/actions/create-check
+        with:
+          name: "Lint All"
+          token: ${{ secrets.GITHUB_TOKEN }}
+          sha: ${{ inputs.check-sha }}
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
+        with:
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Lint
@@ -94,11 +61,11 @@ jobs:
         run: npm run postlint --ignore-scripts
       - name: Conclude Check
         uses: LouisBrunner/[email protected]
-        if: steps.check.outputs.check_id && always()
+        if: steps.create-check.outputs.check-id && always()
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           conclusion: ${{ job.status }}
-          check_id: ${{ steps.check.outputs.check_id }}
+          check_id: ${{ steps.create-check.outputs.check-id }}
 
   test-all:
     name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
@@ -113,6 +80,9 @@ jobs:
           - name: macOS
             os: macos-latest
             shell: bash
+          - name: macOS
+            os: macos-13
+            shell: bash
           - name: Windows
             os: windows-latest
             shell: cmd
@@ -123,84 +93,56 @@ jobs:
           - 14.x
           - 16.x
           - 18.x
+          - 20.x
+          - 22.x
+        exclude:
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 10.0.0
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 10.x
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 12.x
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 14.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 16.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 18.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 20.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 22.x
     runs-on: ${{ matrix.platform.os }}
     defaults:
       run:
         shell: ${{ matrix.platform.shell }}
     steps:
-      - name: Get Workflow Job
-        uses: actions/github-script@v6
-        if: inputs.check-sha
-        id: check-output
-        env:
-          JOB_NAME: "Test All"
-          MATRIX_NAME: " - ${{ matrix.platform.name }} - ${{ matrix.node-version }}"
-        with:
-          script: |
-            const { owner, repo } = context.repo
-
-            const { data } = await github.rest.actions.listJobsForWorkflowRun({
-              owner,
-              repo,
-              run_id: context.runId,
-              per_page: 100
-            })
-
-            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
-            const job = data.jobs.find(j => j.name.endsWith(jobName))
-            const jobUrl = job?.html_url
-
-            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
-
-            let summary = `This check is assosciated with ${shaUrl}\n\n`
-
-            if (jobUrl) {
-              summary += `For run logs, click here: ${jobUrl}`
-            } else {
-              summary += `Run logs could not be found for a job with name: "${jobName}"`
-            }
-
-            return { summary }
-      - name: Create Check
-        uses: LouisBrunner/[email protected]
-        id: check
-        if: inputs.check-sha
-        with:
-          token: ${{ secrets.GITHUB_TOKEN }}
-          status: in_progress
-          name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
-          sha: ${{ inputs.check-sha }}
-          output: ${{ steps.check-output.outputs.result }}
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           ref: ${{ inputs.ref }}
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
+      - name: Create Check
+        id: create-check
+        if: ${{ inputs.check-sha }}
+        uses: ./.github/actions/create-check
+        with:
+          name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}"
+          token: ${{ secrets.GITHUB_TOKEN }}
+          sha: ${{ inputs.check-sha }}
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
           node-version: ${{ matrix.node-version }}
-      - name: Update Windows npm
-        # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
-        if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
-        run: |
-          curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
-          tar xf npm-7.5.4.tgz
-          cd package
-          node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
-          cd ..
-          rmdir /s /q package
-      - name: Install npm@7
-        if: startsWith(matrix.node-version, '10.')
-        run: npm i --prefer-online --no-fund --no-audit -g npm@7
-      - name: Install npm@8
-        if: ${{ !startsWith(matrix.node-version, '10.') }}
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          check-latest: contains(matrix.node-version, '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Add Problem Matcher
@@ -209,8 +151,8 @@ jobs:
         run: npm test --ignore-scripts
       - name: Conclude Check
         uses: LouisBrunner/[email protected]
-        if: steps.check.outputs.check_id && always()
+        if: steps.create-check.outputs.check-id && always()
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           conclusion: ${{ job.status }}
-          check_id: ${{ steps.check.outputs.check_id }}
+          check_id: ${{ steps.create-check.outputs.check-id }}
diff --git .github/workflows/ci.yml .github/workflows/ci.yml
index 90c632b9..4a2724b6 100644
--- .github/workflows/ci.yml
+++ .github/workflows/ci.yml
@@ -8,7 +8,7 @@ on:
   push:
     branches:
       - main
-      - latest
+      - release/v*
   schedule:
     # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1
     - cron: "0 9 * * 1"
@@ -23,19 +23,21 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Lint
@@ -56,6 +58,9 @@ jobs:
           - name: macOS
             os: macos-latest
             shell: bash
+          - name: macOS
+            os: macos-13
+            shell: bash
           - name: Windows
             os: windows-latest
             shell: cmd
@@ -66,39 +71,46 @@ jobs:
           - 14.x
           - 16.x
           - 18.x
+          - 20.x
+          - 22.x
+        exclude:
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 10.0.0
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 10.x
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 12.x
+          - platform: { name: macOS, os: macos-latest, shell: bash }
+            node-version: 14.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 16.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 18.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 20.x
+          - platform: { name: macOS, os: macos-13, shell: bash }
+            node-version: 22.x
     runs-on: ${{ matrix.platform.os }}
     defaults:
       run:
         shell: ${{ matrix.platform.shell }}
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
           node-version: ${{ matrix.node-version }}
-      - name: Update Windows npm
-        # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
-        if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
-        run: |
-          curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
-          tar xf npm-7.5.4.tgz
-          cd package
-          node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
-          cd ..
-          rmdir /s /q package
-      - name: Install npm@7
-        if: startsWith(matrix.node-version, '10.')
-        run: npm i --prefer-online --no-fund --no-audit -g npm@7
-      - name: Install npm@8
-        if: ${{ !startsWith(matrix.node-version, '10.') }}
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          check-latest: contains(matrix.node-version, '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Add Problem Matcher
diff --git .github/workflows/codeql-analysis.yml .github/workflows/codeql-analysis.yml
index 66b9498a..f8b17025 100644
--- .github/workflows/codeql-analysis.yml
+++ .github/workflows/codeql-analysis.yml
@@ -6,11 +6,11 @@ on:
   push:
     branches:
       - main
-      - latest
+      - release/v*
   pull_request:
     branches:
       - main
-      - latest
+      - release/v*
   schedule:
     # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1
     - cron: "0 10 * * 1"
@@ -25,14 +25,14 @@ jobs:
       security-events: write
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@v2
+        uses: github/codeql-action/init@v3
         with:
           languages: javascript
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@v2
+        uses: github/codeql-action/analyze@v3
diff --git .github/workflows/post-dependabot.yml .github/workflows/post-dependabot.yml
index 03c85681..1ea8693c 100644
--- .github/workflows/post-dependabot.yml
+++ .github/workflows/post-dependabot.yml
@@ -17,7 +17,7 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           ref: ${{ github.event.pull_request.head.ref }}
       - name: Setup Git User
@@ -25,13 +25,15 @@ jobs:
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Fetch Dependabot Metadata
@@ -47,7 +49,7 @@ jobs:
         id: flags
         run: |
           dependabot_dir="${{ steps.metadata.outputs.directory }}"
-          if [[ "$dependabot_dir" == "/" ]]; then
+          if [[ "$dependabot_dir" == "/" || "$dependabot_dir" == "/main" ]]; then
             echo "workspace=-iwr" >> $GITHUB_OUTPUT
           else
             # strip leading slash from directory so it works as a
diff --git .github/workflows/pull-request.yml .github/workflows/pull-request.yml
index da5779df..7dbdfd41 100644
--- .github/workflows/pull-request.yml
+++ .github/workflows/pull-request.yml
@@ -20,7 +20,7 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
       - name: Setup Git User
@@ -28,23 +28,23 @@ jobs:
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Run Commitlint on Commits
         id: commit
         continue-on-error: true
-        run: |
-          npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }}
+        run: npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }}
       - name: Run Commitlint on PR Title
         if: steps.commit.outcome == 'failure'
         env:
           PR_TITLE: ${{ github.event.pull_request.title }}
-        run: |
-          echo '$PR_TITLE' | npx --offline commitlint -V
+        run: echo "$PR_TITLE" | npx --offline commitlint -V
diff --git a/.github/workflows/release-integration.yml b/.github/workflows/release-integration.yml
new file mode 100644
index 00000000..130578e6
--- /dev/null
+++ .github/workflows/release-integration.yml
@@ -0,0 +1,70 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Release Integration
+
+on:
+  workflow_dispatch:
+    inputs:
+      releases:
+        required: true
+        type: string
+        description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version'
+  workflow_call:
+    inputs:
+      releases:
+        required: true
+        type: string
+        description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version'
+    secrets:
+      PUBLISH_TOKEN:
+        required: true
+
+jobs:
+  publish:
+    name: Publish
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    permissions:
+      id-token: write
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          ref: ${{ fromJSON(inputs.releases)[0].tagName }}
+      - name: Setup Git User
+        run: |
+          git config --global user.email "[email protected]"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v4
+        id: node
+        with:
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
+        with:
+          node: ${{ steps.node.outputs.node-version }}
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Set npm authToken
+        run: npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN}
+      - name: Publish
+        env:
+          PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
+          RELEASES: ${{ inputs.releases }}
+        run: |
+          EXIT_CODE=0
+
+          for release in $(echo $RELEASES | jq -r '.[] | @base64'); do
+            PUBLISH_TAG=$(echo "$release" | base64 --decode | jq -r .publishTag)
+            npm publish --provenance --tag="$PUBLISH_TAG"
+            STATUS=$?
+            if [[ "$STATUS" -eq 1 ]]; then
+              EXIT_CODE=$STATUS
+            fi
+          done
+
+          exit $EXIT_CODE
diff --git .github/workflows/release.yml .github/workflows/release.yml
index 3b69ae10..e77e76f2 100644
--- .github/workflows/release.yml
+++ .github/workflows/release.yml
@@ -3,15 +3,9 @@
 name: Release
 
 on:
-  workflow_dispatch:
-    inputs:
-      release-pr:
-        description: a release PR number to rerun release jobs on
-        type: string
   push:
     branches:
       - main
-      - latest
       - release/v*
 
 permissions:
@@ -23,12 +17,12 @@ jobs:
   release:
     outputs:
       pr: ${{ steps.release.outputs.pr }}
-      release: ${{ steps.release.outputs.release }}
-      releases: ${{ steps.release.outputs.releases }}
-      branch: ${{ steps.release.outputs.pr-branch }}
+      pr-branch: ${{ steps.release.outputs.pr-branch }}
       pr-number: ${{ steps.release.outputs.pr-number }}
-      comment-id: ${{ steps.pr-comment.outputs.result }}
-      check-id: ${{ steps.check.outputs.check_id }}
+      pr-sha: ${{ steps.release.outputs.pr-sha }}
+      releases: ${{ steps.release.outputs.releases }}
+      comment-id: ${{ steps.create-comment.outputs.comment-id || steps.update-comment.outputs.comment-id }}
+      check-id: ${{ steps.create-check.outputs.check-id }}
     name: Release
     if: github.repository_owner == 'npm'
     runs-on: ubuntu-latest
@@ -37,108 +31,75 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
+        with:
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
       - name: Release Please
         id: release
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        run: |
-          npx --offline template-oss-release-please "${{ github.ref_name }}" "${{ inputs.release-pr }}"
-      - name: Post Pull Request Comment
+        run: npx --offline template-oss-release-please --branch="${{ github.ref_name }}" --backport="" --defaultTag="latest"
+      - name: Create Release Manager Comment Text
         if: steps.release.outputs.pr-number
-        uses: actions/github-script@v6
-        id: pr-comment
-        env:
-          PR_NUMBER: ${{ steps.release.outputs.pr-number }}
-          REF_NAME: ${{ github.ref_name }}
+        uses: actions/github-script@v7
+        id: comment-text
         with:
+          result-encoding: string
           script: |
-            const { REF_NAME, PR_NUMBER: issue_number } = process.env
             const { runId, repo: { owner, repo } } = context
-
             const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId })
-
-            let body = '## Release Manager\n\n'
-
-            const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
-            let commentId = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id
-
-            body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n`
-            body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`main\`. `
-            body += `To force CI to update this PR, run this command:\n\n`
-            body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${issue_number}\n\`\`\``
-
-            if (commentId) {
-              await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body })
-            } else {
-              const { data: comment } = await github.rest.issues.createComment({ owner, repo, issue_number, body })
-              commentId = comment?.id
-            }
-
-            return commentId
-      - name: Get Workflow Job
-        uses: actions/github-script@v6
-        if: steps.release.outputs.pr-sha
-        id: check-output
-        env:
-          JOB_NAME: "Release"
-          MATRIX_NAME: ""
+            return['## Release Manager', `Release workflow run: ${workflow.html_url}`].join('\n\n')
+      - name: Find Release Manager Comment
+        uses: peter-evans/find-comment@v2
+        if: steps.release.outputs.pr-number
+        id: found-comment
         with:
-          script: |
-            const { owner, repo } = context.repo
-
-            const { data } = await github.rest.actions.listJobsForWorkflowRun({
-              owner,
-              repo,
-              run_id: context.runId,
-              per_page: 100
-            })
-
-            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
-            const job = data.jobs.find(j => j.name.endsWith(jobName))
-            const jobUrl = job?.html_url
-
-            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.release.outputs.pr-sha }}`
-
-            let summary = `This check is assosciated with ${shaUrl}\n\n`
-
-            if (jobUrl) {
-              summary += `For run logs, click here: ${jobUrl}`
-            } else {
-              summary += `Run logs could not be found for a job with name: "${jobName}"`
-            }
-
-            return { summary }
+          issue-number: ${{ steps.release.outputs.pr-number }}
+          comment-author: 'github-actions[bot]'
+          body-includes: '## Release Manager'
+      - name: Create Release Manager Comment
+        id: create-comment
+        if: steps.release.outputs.pr-number && !steps.found-comment.outputs.comment-id
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          issue-number: ${{ steps.release.outputs.pr-number }}
+          body: ${{ steps.comment-text.outputs.result }}
+      - name: Update Release Manager Comment
+        id: update-comment
+        if: steps.release.outputs.pr-number && steps.found-comment.outputs.comment-id
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          comment-id: ${{ steps.found-comment.outputs.comment-id }}
+          body: ${{ steps.comment-text.outputs.result }}
+          edit-mode: 'replace'
       - name: Create Check
-        uses: LouisBrunner/[email protected]
-        id: check
+        id: create-check
+        uses: ./.github/actions/create-check
         if: steps.release.outputs.pr-sha
         with:
+          name: "Release"
           token: ${{ secrets.GITHUB_TOKEN }}
-          status: in_progress
-          name: Release
           sha: ${{ steps.release.outputs.pr-sha }}
-          output: ${{ steps.check-output.outputs.result }}
 
   update:
     needs: release
     outputs:
       sha: ${{ steps.commit.outputs.sha }}
-      check-id: ${{ steps.check.outputs.check_id }}
+      check-id: ${{ steps.create-check.outputs.check-id }}
     name: Update - Release
     if: github.repository_owner == 'npm' && needs.release.outputs.pr
     runs-on: ubuntu-latest
@@ -147,32 +108,41 @@ jobs:
         shell: bash
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
-          ref: ${{ needs.release.outputs.branch }}
+          ref: ${{ needs.release.outputs.pr-branch }}
       - name: Setup Git User
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "npm CLI robot"
       - name: Setup Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
+        id: node
+        with:
+          node-version: 22.x
+          check-latest: contains('22.x', '.x')
+      - name: Install Latest npm
+        uses: ./.github/actions/install-latest-npm
         with:
-          node-version: 18.x
-      - name: Install npm@8
-        run: npm i --prefer-online --no-fund --no-audit -g npm@8
-      - name: npm Version
-        run: npm -v
+          node: ${{ steps.node.outputs.node-version }}
       - name: Install Dependencies
         run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Create Release Manager Checklist Text
+        id: comment-text
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: npm exec --offline -- template-oss-release-manager --pr="${{ needs.release.outputs.pr-number }}" --backport="" --defaultTag="latest" --publish
+      - name: Append Release Manager Comment
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          comment-id: ${{ needs.release.outputs.comment-id }}
+          body: ${{ steps.comment-text.outputs.result }}
+          edit-mode: 'append'
       - name: Run Post Pull Request Actions
         env:
-          RELEASE_PR_NUMBER: ${{ needs.release.outputs.pr-number }}
-          RELEASE_COMMENT_ID: ${{ needs.release.outputs.comment-id }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        run: |
-          npm exec --offline -- template-oss-release-manager --lockfile=false --publish=true
-          npm run rp-pull-request --ignore-scripts --if-present
+        run: npm run rp-pull-request --ignore-scripts --if-present -- --pr="${{ needs.release.outputs.pr-number }}" --commentId="${{ needs.release.outputs.comment-id }}"
       - name: Commit
         id: commit
         env:
@@ -181,52 +151,16 @@ jobs:
           git commit --all --amend --no-edit || true
           git push --force-with-lease
           echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-      - name: Get Workflow Job
-        uses: actions/github-script@v6
-        if: steps.commit.outputs.sha
-        id: check-output
-        env:
-          JOB_NAME: "Update - Release"
-          MATRIX_NAME: ""
-        with:
-          script: |
-            const { owner, repo } = context.repo
-
-            const { data } = await github.rest.actions.listJobsForWorkflowRun({
-              owner,
-              repo,
-              run_id: context.runId,
-              per_page: 100
-            })
-
-            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
-            const job = data.jobs.find(j => j.name.endsWith(jobName))
-            const jobUrl = job?.html_url
-
-            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.commit.outputs.sha }}`
-
-            let summary = `This check is assosciated with ${shaUrl}\n\n`
-
-            if (jobUrl) {
-              summary += `For run logs, click here: ${jobUrl}`
-            } else {
-              summary += `Run logs could not be found for a job with name: "${jobName}"`
-            }
-
-            return { summary }
       - name: Create Check
-        uses: LouisBrunner/[email protected]
-        id: check
-        if: steps.commit.outputs.sha
+        id: create-check
+        uses: ./.github/actions/create-check
         with:
+          name: "Update - Release"
+          check-name: "Release"
           token: ${{ secrets.GITHUB_TOKEN }}
-          status: in_progress
-          name: Release
           sha: ${{ steps.commit.outputs.sha }}
-          output: ${{ steps.check-output.outputs.result }}
       - name: Conclude Check
         uses: LouisBrunner/[email protected]
-        if: needs.release.outputs.check-id && always()
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           conclusion: ${{ job.status }}
@@ -238,7 +172,7 @@ jobs:
     if: needs.release.outputs.pr
     uses: ./.github/workflows/ci-release.yml
     with:
-      ref: ${{ needs.release.outputs.branch }}
+      ref: ${{ needs.release.outputs.pr-branch }}
       check-sha: ${{ needs.update.outputs.sha }}
 
   post-ci:
@@ -250,8 +184,8 @@ jobs:
       run:
         shell: bash
     steps:
-      - name: Get Needs Result
-        id: needs-result
+      - name: Get CI Conclusion
+        id: conclusion
         run: |
           result=""
           if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
@@ -264,14 +198,15 @@ jobs:
           echo "result=$result" >> $GITHUB_OUTPUT
       - name: Conclude Check
         uses: LouisBrunner/[email protected]
-        if: needs.update.outputs.check-id && always()
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
-          conclusion: ${{ steps.needs-result.outputs.result }}
+          conclusion: ${{ steps.conclusion.outputs.result }}
           check_id: ${{ needs.update.outputs.check-id }}
 
   post-release:
     needs: release
+    outputs:
+      comment-id: ${{ steps.create-comment.outputs.comment-id }}
     name: Post Release - Release
     if: github.repository_owner == 'npm' && needs.release.outputs.releases
     runs-on: ubuntu-latest
@@ -279,79 +214,54 @@ jobs:
       run:
         shell: bash
     steps:
-      - name: Create Release PR Comment
-        uses: actions/github-script@v6
+      - name: Create Release PR Comment Text
+        id: comment-text
+        uses: actions/github-script@v7
         env:
           RELEASES: ${{ needs.release.outputs.releases }}
         with:
+          result-encoding: string
           script: |
             const releases = JSON.parse(process.env.RELEASES)
             const { runId, repo: { owner, repo } } = context
             const issue_number = releases[0].prNumber
-
-            let body = '## Release Workflow\n\n'
-            for (const { pkgName, version, url } of releases) {
-              body += `- \`${pkgName}@${version}\` ${url}\n`
-            }
-
-            const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
-              .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body })))
-            console.log(`Found comments: ${JSON.stringify(comments, null, 2)}`)
-            const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at'))
-
-            for (const comment of releaseComments) {
-              console.log(`Release comment: ${JSON.stringify(comment, null, 2)}`)
-              await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id })
-            }
-
             const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`
-            await github.rest.issues.createComment({
-              owner,
-              repo,
-              issue_number,
-              body: `${body}- Workflow run: :arrows_counterclockwise: ${runUrl}`,
-            })
+
+            return [
+              '## Release Workflow\n',
+              ...releases.map(r => `- \`${r.pkgName}@${r.version}\` ${r.url}`),
+              `- Workflow run: :arrows_counterclockwise: ${runUrl}`,
+            ].join('\n')
+      - name: Create Release PR Comment
+        id: create-comment
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }}
+          body: ${{ steps.comment-text.outputs.result }}
 
   release-integration:
     needs: release
     name: Release Integration
-    if: needs.release.outputs.release
-    runs-on: ubuntu-latest
-    defaults:
-      run:
-        shell: bash
+    if: needs.release.outputs.releases
+    uses: ./.github/workflows/release-integration.yml
     permissions:
-      deployments: write
       id-token: write
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          ref: ${{ fromJSON(needs.release.outputs.release).tagName }}
-      - name: Setup Node
-        uses: actions/setup-node@v3
-        with:
-          node-version: 18.x
-      - name: Install npm@latest
-        run: |
-          npm i --prefer-online --no-fund --no-audit -g npm@latest
-          npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN}
-      - name: Publish
-        env:
-          PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
-        run: npm publish --provenance
+    secrets:
+      PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
+    with:
+      releases: ${{ needs.release.outputs.releases }}
 
   post-release-integration:
-    needs: [ release, release-integration ]
+    needs: [ release, release-integration, post-release ]
     name: Post Release Integration - Release
-    if: github.repository_owner == 'npm' && needs.release.outputs.release && always()
+    if: github.repository_owner == 'npm' && needs.release.outputs.releases && always()
     runs-on: ubuntu-latest
     defaults:
       run:
         shell: bash
     steps:
-      - name: Get Needs Result
-        id: needs-result
+      - name: Get Post Release Conclusion
+        id: conclusion
         run: |
           if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
             result="x"
@@ -361,39 +271,38 @@ jobs:
             result="white_check_mark"
           fi
           echo "result=$result" >> $GITHUB_OUTPUT
-      - name: Update Release PR Comment
-        uses: actions/github-script@v6
+      - name: Find Release PR Comment
+        uses: peter-evans/find-comment@v2
+        id: found-comment
+        with:
+          issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }}
+          comment-author: 'github-actions[bot]'
+          body-includes: '## Release Workflow'
+      - name: Create Release PR Comment Text
+        id: comment-text
+        if: steps.found-comment.outputs.comment-id
+        uses: actions/github-script@v7
         env:
-          PR_NUMBER: ${{ fromJSON(needs.release.outputs.release).prNumber }}
-          RESULT: ${{ steps.needs-result.outputs.result }}
+          RESULT: ${{ steps.conclusion.outputs.result }}
+          BODY: ${{ steps.found-comment.outputs.comment-body }}
         with:
+          result-encoding: string
           script: |
-            const { PR_NUMBER: issue_number, RESULT } = process.env
-            const { runId, repo: { owner, repo } } = context
-
-            const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
-            const updateComment = comments.find(c =>
-              c.user.login === 'github-actions[bot]' &&
-              c.body.startsWith('## Release Workflow\n\n') &&
-              c.body.includes(runId)
-            )
-
-            if (updateComment) {
-              console.log('Found comment to update:', JSON.stringify(updateComment, null, 2))
-              let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, `Workflow run: :${RESULT}:`)
-              const tagCodeowner = RESULT !== 'white_check_mark'
-              if (tagCodeowner) {
-                body += `\n\n:rotating_light:`
-                body += ` @npm/cli-team: The post-release workflow failed for this release.`
-                body += ` Manual steps may need to be taken after examining the workflow output`
-                body += ` from the above workflow run. :rotating_light:`
-              }
-              await github.rest.issues.updateComment({
-                owner,
-                repo,
-                body,
-                comment_id: updateComment.id,
-              })
-            } else {
-              console.log('No matching comments found:', JSON.stringify(comments, null, 2))
+            const { RESULT, BODY } = process.env
+            const body = [BODY.replace(/(Workflow run: :)[a-z_]+(:)/, `$1${RESULT}$2`)]
+            if (RESULT !== 'white_check_mark') {
+              body.push(':rotating_light::rotating_light::rotating_light:')
+              body.push([
+                '@npm/cli-team: The post-release workflow failed for this release.',
+                'Manual steps may need to be taken after examining the workflow output.'
+              ].join(' '))
+              body.push(':rotating_light::rotating_light::rotating_light:')
             }
+            return body.join('\n\n').trim()
+      - name: Update Release PR Comment
+        if: steps.comment-text.outputs.result
+        uses: peter-evans/create-or-update-comment@v3
+        with:
+          comment-id: ${{ steps.found-comment.outputs.comment-id }}
+          body: ${{ steps.comment-text.outputs.result }}
+          edit-mode: 'replace'
diff --git .gitignore .gitignore
index 00bdaf2b..ac3c1dad 100644
--- .gitignore
+++ .gitignore
@@ -3,15 +3,18 @@
 # ignore everything in the root
 /*
 
-# keep these
 !**/.gitignore
 !/.commitlintrc.js
 !/.eslintrc.js
 !/.eslintrc.local.*
+!/.git-blame-ignore-revs
 !/.github/
 !/.gitignore
 !/.npmrc
+!/.prettierignore
+!/.prettierrc.js
 !/.release-please-manifest.json
+!/benchmarks
 !/bin/
 !/CHANGELOG*
 !/classes/
@@ -34,3 +37,5 @@
 !/SECURITY.md
 !/tap-snapshots/
 !/test/
+!/tsconfig.json
+tap-testdir*/
diff --git .release-please-manifest.json .release-please-manifest.json
index cc729eee..df0299e9 100644
--- .release-please-manifest.json
+++ .release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "7.5.3"
+  ".": "7.7.1"
 }
diff --git CHANGELOG.md CHANGELOG.md
index 292ed24a..f80a7622 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,5 +1,90 @@
 # Changelog
 
+## [7.7.1](https://github.com/npm/node-semver/compare/v7.7.0...v7.7.1) (2025-02-03)
+### Bug Fixes
+* [`af761c0`](https://github.com/npm/node-semver/commit/af761c05bd53eef83b5e20f8b09360b0e70557dc) [#764](https://github.com/npm/node-semver/pull/764) inc: fully capture prerelease identifier (#764) (@wraithgar)
+
+## [7.7.0](https://github.com/npm/node-semver/compare/v7.6.3...v7.7.0) (2025-01-29)
+### Features
+* [`0864b3c`](https://github.com/npm/node-semver/commit/0864b3ce7932667013e0c7c5ec764777d4682883) [#753](https://github.com/npm/node-semver/pull/753) add "release" inc type (#753) (@mbtools)
+### Bug Fixes
+* [`d588e37`](https://github.com/npm/node-semver/commit/d588e3782864b1cab2fe9f2452b848e8c7f609d1) [#755](https://github.com/npm/node-semver/pull/755) diff: fix prerelease to stable version diff logic (#755) (@eminberkayd, berkay.daglar)
+* [`8a34bde`](https://github.com/npm/node-semver/commit/8a34bdecc783407f4e1a8a1ee1f67906b84a4b78) [#754](https://github.com/npm/node-semver/pull/754) add identifier validation to `inc()` (#754) (@mbtools)
+### Documentation
+* [`67e5478`](https://github.com/npm/node-semver/commit/67e54785a0f871361230f84323cbb631b9b6d834) [#756](https://github.com/npm/node-semver/pull/756) readme: added missing period for consistency (#756) (@shaymolcho)
+* [`868d4bb`](https://github.com/npm/node-semver/commit/868d4bbe3d318c52544f38d5f9977a1103e924c2) [#749](https://github.com/npm/node-semver/pull/749) clarify comment about obsolete prefixes (#749) (@mbtools, @ljharb)
+### Chores
+* [`145c554`](https://github.com/npm/node-semver/commit/145c554b8c7b7ecfcb451153ad18bdb2f24ad10d) [#741](https://github.com/npm/node-semver/pull/741) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (@dependabot[bot])
+* [`753e02b`](https://github.com/npm/node-semver/commit/753e02b9d0cb3ac23e085dc33efcab3e08d61f2b) [#747](https://github.com/npm/node-semver/pull/747) bump @npmcli/template-oss from 4.23.3 to 4.23.4 (#747) (@dependabot[bot], @npm-cli-bot)
+* [`0b812d5`](https://github.com/npm/node-semver/commit/0b812d5fb5fbb208e89dc1250e2efafeaa549437) [#744](https://github.com/npm/node-semver/pull/744) postinstall for dependabot template-oss PR (@hashtagchris)
+
+## [7.6.3](https://github.com/npm/node-semver/compare/v7.6.2...v7.6.3) (2024-07-16)
+
+### Bug Fixes
+
+* [`73a3d79`](https://github.com/npm/node-semver/commit/73a3d79c4ec32d5dd62c9d5f64e5af7fbdad9ec0) [#726](https://github.com/npm/node-semver/pull/726) optimize Range parsing and formatting (#726) (@jviide)
+
+### Documentation
+
+* [`2975ece`](https://github.com/npm/node-semver/commit/2975ece120e17660c9f1ef517de45c09ff821064) [#719](https://github.com/npm/node-semver/pull/719) fix extra backtick typo (#719) (@stdavis)
+
+## [7.6.2](https://github.com/npm/node-semver/compare/v7.6.1...v7.6.2) (2024-05-09)
+
+### Bug Fixes
+
+* [`6466ba9`](https://github.com/npm/node-semver/commit/6466ba9b540252db405fdd2a289dd4651495beea) [#713](https://github.com/npm/node-semver/pull/713) lru: use map.delete() directly (#713) (@negezor, @lukekarrys)
+
+## [7.6.1](https://github.com/npm/node-semver/compare/v7.6.0...v7.6.1) (2024-05-04)
+
+### Bug Fixes
+
+* [`c570a34`](https://github.com/npm/node-semver/commit/c570a348ffc6612af07fe94fa46b9affa5e4eff0) [#704](https://github.com/npm/node-semver/pull/704) linting: no-unused-vars (@wraithgar)
+* [`ad8ff11`](https://github.com/npm/node-semver/commit/ad8ff11dd200dac3a05097d9a82d1977ccfa1535) [#704](https://github.com/npm/node-semver/pull/704) use internal cache implementation (@mbtools)
+* [`ac9b357`](https://github.com/npm/node-semver/commit/ac9b35769ab0ddfefd5a3af4a3ecaf3da2012352) [#682](https://github.com/npm/node-semver/pull/682) typo in compareBuild debug message (#682) (@mbtools)
+
+### Dependencies
+
+* [`988a8de`](https://github.com/npm/node-semver/commit/988a8deb3ea76b9a314a740e66b5fc2f726822f8) [#709](https://github.com/npm/node-semver/pull/709) uninstall `lru-cache` (#709)
+* [`3fabe4d`](https://github.com/npm/node-semver/commit/3fabe4dbfbd199fdb589c076a7f30bc1f18c6614) [#704](https://github.com/npm/node-semver/pull/704) remove lru-cache
+
+### Chores
+
+* [`dd09b60`](https://github.com/npm/node-semver/commit/dd09b60da1e618335d7c269426345b336fd5f63d) [#705](https://github.com/npm/node-semver/pull/705) bump @npmcli/template-oss to 4.22.0 (@lukekarrys)
+* [`ec49cdc`](https://github.com/npm/node-semver/commit/ec49cdcece9db0020d6829b246681ff65a393644) [#701](https://github.com/npm/node-semver/pull/701) chore: chore: postinstall for dependabot template-oss PR (@lukekarrys)
+* [`b236c3d`](https://github.com/npm/node-semver/commit/b236c3d2f357a16a733c96ec2ca8c57848b70091) [#696](https://github.com/npm/node-semver/pull/696) add benchmarks (#696) (@H4ad)
+* [`692451b`](https://github.com/npm/node-semver/commit/692451bd6f75b38a71a99f39da405c94a5954a22) [#688](https://github.com/npm/node-semver/pull/688) various improvements to README (#688) (@mbtools)
+* [`5feeb7f`](https://github.com/npm/node-semver/commit/5feeb7f4f63061e19a29087115b50cb04135b63e) [#705](https://github.com/npm/node-semver/pull/705) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`074156f`](https://github.com/npm/node-semver/commit/074156f64fa91723fe1ae6af8cc497014b9b7aff) [#701](https://github.com/npm/node-semver/pull/701) bump @npmcli/template-oss from 4.21.3 to 4.21.4 (@dependabot[bot])
+
+## [7.6.0](https://github.com/npm/node-semver/compare/v7.5.4...v7.6.0) (2024-01-31)
+
+### Features
+
+* [`a7ab13a`](https://github.com/npm/node-semver/commit/a7ab13a46201e342d34e84a989632b380f755baf) [#671](https://github.com/npm/node-semver/pull/671) preserve pre-release and build parts of a version on coerce (#671) (@madtisa, madtisa, @wraithgar)
+
+### Chores
+
+* [`816c7b2`](https://github.com/npm/node-semver/commit/816c7b2cbfcb1986958a290f941eddfd0441139e) [#667](https://github.com/npm/node-semver/pull/667) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`0bd24d9`](https://github.com/npm/node-semver/commit/0bd24d943cbd1a7f6a2b8d384590bfa98559e1de) [#667](https://github.com/npm/node-semver/pull/667) bump @npmcli/template-oss from 4.21.1 to 4.21.3 (@dependabot[bot])
+* [`e521932`](https://github.com/npm/node-semver/commit/e521932f115a81030f4e7c34e8631cdd3c6a108b) [#652](https://github.com/npm/node-semver/pull/652) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`8873991`](https://github.com/npm/node-semver/commit/88739918080debeb239aae840b35c07436148e50) [#652](https://github.com/npm/node-semver/pull/652) chore: chore: postinstall for dependabot template-oss PR (@lukekarrys)
+* [`f317dc8`](https://github.com/npm/node-semver/commit/f317dc8689781bcfd98e2c32b46157276acdd47c) [#652](https://github.com/npm/node-semver/pull/652) bump @npmcli/template-oss from 4.19.0 to 4.21.0 (@dependabot[bot])
+* [`7303db1`](https://github.com/npm/node-semver/commit/7303db1fe54d6905b23ccb0162878e37d73535ef) [#658](https://github.com/npm/node-semver/pull/658) add clean() test for build metadata (#658) (@jethrodaniel)
+* [`6240d75`](https://github.com/npm/node-semver/commit/6240d75a7c620b0a222f05969a91fdc3dc2be0fb) [#656](https://github.com/npm/node-semver/pull/656) add missing quotes in README.md (#656) (@zyxkad)
+* [`14d263f`](https://github.com/npm/node-semver/commit/14d263faa156e408a033b9b12a2f87735c2df42c) [#625](https://github.com/npm/node-semver/pull/625) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`7c34e1a`](https://github.com/npm/node-semver/commit/7c34e1ac1bcc0bc6579b30745c96075c69bd0332) [#625](https://github.com/npm/node-semver/pull/625) bump @npmcli/template-oss from 4.18.1 to 4.19.0 (@dependabot[bot])
+* [`123e0b0`](https://github.com/npm/node-semver/commit/123e0b03287e1af295ef82d55f55c16805596f35) [#622](https://github.com/npm/node-semver/pull/622) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`737d5e1`](https://github.com/npm/node-semver/commit/737d5e1cf10e631bab8a28594aa2d5c9d4090814) [#622](https://github.com/npm/node-semver/pull/622) bump @npmcli/template-oss from 4.18.0 to 4.18.1 (@dependabot[bot])
+* [`cce6180`](https://github.com/npm/node-semver/commit/cce61804ba6f997225a1267135c06676fe0524d2) [#598](https://github.com/npm/node-semver/pull/598) postinstall for dependabot template-oss PR (@lukekarrys)
+* [`b914a3d`](https://github.com/npm/node-semver/commit/b914a3d0d26ca27d2685053d7d390af4e02eedd9) [#598](https://github.com/npm/node-semver/pull/598) bump @npmcli/template-oss from 4.17.0 to 4.18.0 (@dependabot[bot])
+
+## [7.5.4](https://github.com/npm/node-semver/compare/v7.5.3...v7.5.4) (2023-07-07)
+
+### Bug Fixes
+
+* [`cc6fde2`](https://github.com/npm/node-semver/commit/cc6fde2d34b95cb600d126649d926901bd2a9703) [#588](https://github.com/npm/node-semver/pull/588) trim each range set before parsing (@lukekarrys)
+* [`99d8287`](https://github.com/npm/node-semver/commit/99d8287516a1d2abf0286033e2e26eca6b69c09f) [#583](https://github.com/npm/node-semver/pull/583) correctly parse long build ids as valid (#583) (@lukekarrys)
+
 ## [7.5.3](https://github.com/npm/node-semver/compare/v7.5.2...v7.5.3) (2023-06-22)
 
 ### Bug Fixes
diff --git README.md README.md
index 53ea9b52..e9522153 100644
--- README.md
+++ README.md
@@ -25,7 +25,7 @@ semver.valid(semver.coerce('v2')) // '2.0.0'
 semver.valid(semver.coerce('42.6.7.9.3-alpha')) // '42.6.7'
 \`\`\`
 
-You can also just load the module for the function that you care about, if
+You can also just load the module for the function that you care about if
 you'd like to minimize your footprint.
 
 ```js
@@ -78,8 +78,8 @@ const semverOutside = require('semver/ranges/outside')
 const semverGtr = require('semver/ranges/gtr')
 const semverLtr = require('semver/ranges/ltr')
 const semverIntersects = require('semver/ranges/intersects')
-const simplifyRange = require('semver/ranges/simplify')
-const rangeSubset = require('semver/ranges/subset')
+const semverSimplifyRange = require('semver/ranges/simplify')
+const semverRangeSubset = require('semver/ranges/subset')

As a command-line utility:
@@ -100,7 +100,7 @@ Options:
-i --increment []
Increment a version by the specified level. Level can
be one of: major, minor, patch, premajor, preminor,

  •    prepatch, or prerelease.  Default level is 'patch'.
    
  •    prepatch, prerelease, or release.  Default level is 'patch'.
       Only one version may be specified.
    

--preid
@@ -141,10 +141,12 @@ A "version" is described by the v2.0.0 specification found at
https://semver.org/.

A leading "=" or "v" character is stripped off and ignored.
+Support for stripping a leading "v" is kept for compatibility with v1.0.0 of the SemVer
+specification but should not be used anymore.

Ranges

-A version range is a set of comparators which specify versions
+A version range is a set of comparators that specify versions
that satisfy the range.

A comparator is composed of an operator and a version. The set
@@ -155,7 +157,7 @@ of primitive operators is:

  • > Greater than
  • >= Greater than or equal to
  • = Equal. If no operator is specified, then equality is assumed,
  • so this operator is optional, but MAY be included.
  • so this operator is optional but MAY be included.

For example, the comparator >=1.2.7 would match the versions
1.2.7, 1.2.8, 2.5.3, and 1.3.9, but not the versions 1.2.6
@@ -189,26 +191,26 @@ For example, the range >1.2.3-alpha.3 would be allowed to match the
version 1.2.3-alpha.7, but it would not be satisfied by
3.4.5-alpha.9, even though 3.4.5-alpha.9 is technically "greater
than" 1.2.3-alpha.3 according to the SemVer sort rules. The version
-range only accepts prerelease tags on the 1.2.3 version. The
-version 3.4.5 would satisfy the range, because it does not have a
+range only accepts prerelease tags on the 1.2.3 version.
+Version 3.4.5 would satisfy the range because it does not have a
prerelease flag, and 3.4.5 is greater than 1.2.3-alpha.7.

-The purpose for this behavior is twofold. First, prerelease versions
+The purpose of this behavior is twofold. First, prerelease versions
frequently are updated very quickly, and contain many breaking changes
that are (by the author's design) not yet fit for public consumption.
-Therefore, by default, they are excluded from range matching
+Therefore, by default, they are excluded from range-matching
semantics.

Second, a user who has opted into using a prerelease version has
-clearly indicated the intent to use that specific set of
+indicated the intent to use that specific set of
alpha/beta/rc versions. By including a prerelease tag in the range,
the user is indicating that they are aware of the risk. However, it
is still not appropriate to assume that they have opted into taking a
similar risk on the next set of prerelease versions.

Note that this behavior can be suppressed (treating all prerelease
-versions as if they were normal versions, for the purpose of range
-matching) by setting the includePrerelease flag on the options
+versions as if they were normal versions, for range-matching)
+by setting the includePrerelease flag on the options
object to any
functions that do
range matching.
@@ -237,6 +239,13 @@ $ semver 1.2.4-beta.0 -i prerelease
1.2.4-beta.1


+To get out of the prerelease phase, use the `release` option:
+
+```bash
+$ semver 1.2.4-beta.1 -i release
+1.2.4
+```
+
#### Prerelease Identifier Base

The method `.inc` takes an optional parameter 'identifierBase' string
@@ -401,12 +410,12 @@ All methods and classes take a final `options` object argument.  All
options in this object are `false` by default.  The options supported
are:

-- `loose`  Be more forgiving about not-quite-valid semver strings.
+- `loose`: Be more forgiving about not-quite-valid semver strings.
  (Any resulting output will always be 100% strict compliant, of
  course.)  For backwards compatibility reasons, if the `options`
  argument is a boolean value instead of an object, it is interpreted
  to be the `loose` param.
-- `includePrerelease`  Set to suppress the [default
+- `includePrerelease`: Set to suppress the [default
  behavior](https://github.com/npm/node-semver#prerelease-tags) of
  excluding prerelease tagged versions from ranges unless they are
  explicitly opted into.
@@ -415,16 +424,21 @@ Strict-mode Comparators and Ranges will be strict about the SemVer
strings that they parse.

* `valid(v)`: Return the parsed version, or null if it's not valid.
-* `inc(v, release)`: Return the version incremented by the release
-  type (`major`,   `premajor`, `minor`, `preminor`, `patch`,
-  `prepatch`, or `prerelease`), or null if it's not valid
+* `inc(v, releaseType, options, identifier, identifierBase)`: 
+  Return the version incremented by the release
+  type (`major`, `premajor`, `minor`, `preminor`, `patch`,
+  `prepatch`, `prerelease`, or `release`), or null if it's not valid
  * `premajor` in one call will bump the version up to the next major
    version and down to a prerelease of that major version.
    `preminor`, an,d `prepatch` work the same way.
-  * If called from a non-prerelease version, the `prerelease` will work the
-    same as `prepatch`. It increments the patch version, then makes a
+  * If called from a non-prerelease version, `prerelease` will work the
+    same as `prepatch`. It increments the patch version and then makes a
    prerelease. If the input version is already a prerelease it simply
    increments it.
+  * `release` will remove any prerelease part of the version.
+  * `identifier` can be used to prefix `premajor`, `preminor`,
+    `prepatch`, or `prerelease` version increments. `identifierBase`
+    is the base to be used for the `prerelease` identifier.
* `prerelease(v)`: Returns an array of prerelease components, or null
  if none exist. Example: `prerelease('1.2.3-alpha.1') -> ['alpha', 1]`
* `major(v)`: Return the major version number.
@@ -442,7 +456,7 @@ strings that they parse.
* `lt(v1, v2)`: `v1 < v2`
* `lte(v1, v2)`: `v1 <= v2`
* `eq(v1, v2)`: `v1 == v2` This is true if they're logically equivalent,
-  even if they're not the exact same string.  You already know how to
+  even if they're not the same string.  You already know how to
  compare strings.
* `neq(v1, v2)`: `v1 != v2` The opposite of `eq`.
* `cmp(v1, comparator, v2)`: Pass in a comparison string, and it'll call
@@ -451,41 +465,48 @@ strings that they parse.
  invalid comparison string is provided.
* `compare(v1, v2)`: Return `0` if `v1 == v2`, or `1` if `v1` is greater, or `-1` if
  `v2` is greater.  Sorts in ascending order if passed to `Array.sort()`.
-* `rcompare(v1, v2)`: The reverse of compare.  Sorts an array of versions
+* `rcompare(v1, v2)`: The reverse of `compare`.  Sorts an array of versions
  in descending order when passed to `Array.sort()`.
* `compareBuild(v1, v2)`: The same as `compare` but considers `build` when two versions
  are equal.  Sorts in ascending order if passed to `Array.sort()`.
-  `v2` is greater.  Sorts in ascending order if passed to `Array.sort()`.
-* `diff(v1, v2)`: Returns difference between two versions by the release type
+* `compareLoose(v1, v2)`: Short for `compare(v1, v2, { loose: true })`.
+* `diff(v1, v2)`: Returns the difference between two versions by the release type
  (`major`, `premajor`, `minor`, `preminor`, `patch`, `prepatch`, or `prerelease`),
  or null if the versions are the same.

+### Sorting
+
+* `sort(versions)`: Returns a sorted array of versions based on the `compareBuild` 
+  function.
+* `rsort(versions)`: The reverse of `sort`. Returns an array of versions based on
+  the `compareBuild` function in descending order.
+
### Comparators

* `intersects(comparator)`: Return true if the comparators intersect

### Ranges

-* `validRange(range)`: Return the valid range or null if it's not valid
+* `validRange(range)`: Return the valid range or null if it's not valid.
* `satisfies(version, range)`: Return true if the version satisfies the
  range.
* `maxSatisfying(versions, range)`: Return the highest version in the list
  that satisfies the range, or `null` if none of them do.
* `minSatisfying(versions, range)`: Return the lowest version in the list
  that satisfies the range, or `null` if none of them do.
-* `minVersion(range)`: Return the lowest version that can possibly match
+* `minVersion(range)`: Return the lowest version that can match
  the given range.
-* `gtr(version, range)`: Return `true` if version is greater than all the
+* `gtr(version, range)`: Return `true` if the version is greater than all the
  versions possible in the range.
-* `ltr(version, range)`: Return `true` if version is less than all the
+* `ltr(version, range)`: Return `true` if the version is less than all the
  versions possible in the range.
* `outside(version, range, hilo)`: Return true if the version is outside
  the bounds of the range in either the high or low direction.  The
  `hilo` argument must be either the string `'>'` or `'<'`.  (This is
  the function called by `gtr` and `ltr`.)
-* `intersects(range)`: Return true if any of the ranges comparators intersect
+* `intersects(range)`: Return true if any of the range comparators intersect.
* `simplifyRange(versions, range)`: Return a "simplified" range that
-  matches the same items in `versions` list as the range specified.  Note
+  matches the same items in the `versions` list as the range specified.  Note
  that it does *not* guarantee that it would match the same versions in all
  cases, only for the set of versions provided.  This is useful when
  generating ranges by joining together multiple versions with `||`
@@ -498,7 +519,7 @@ strings that they parse.
Note that, since ranges may be non-contiguous, a version might not be
greater than a range, less than a range, *or* satisfy a range!  For
example, the range `1.2 <1.2.9 || >2.0.0` would have a hole from `1.2.9`
-until `2.0.0`, so the version `1.2.10` would not be greater than the
+until `2.0.0`, so version `1.2.10` would not be greater than the
range (because `2.0.1` satisfies, which is higher), nor less than the
range (since `1.2.8` satisfies, which is lower), and it also does not
satisfy the range.
@@ -511,13 +532,13 @@ range, use the `satisfies(version, range)` function.
* `coerce(version, options)`: Coerces a string to semver if possible

This aims to provide a very forgiving translation of a non-semver string to
-semver. It looks for the first digit in a string, and consumes all
+semver. It looks for the first digit in a string and consumes all
remaining characters which satisfy at least a partial semver (e.g., `1`,
`1.2`, `1.2.3`) up to the max permitted length (256 characters).  Longer
versions are simply truncated (`4.6.3.9.2-alpha2` becomes `4.6.3`).  All
surrounding text is simply ignored (`v3.4 replaces v3.3.1` becomes
`3.4.0`).  Only text which lacks digits will fail coercion (`version one`
-is not valid).  The maximum  length for any semver component considered for
+is not valid).  The maximum length for any semver component considered for
coercion is 16 characters; longer components will be ignored
(`10000000000000000.4.7.4` becomes `4.7.4`).  The maximum value for any
semver component is `Number.MAX_SAFE_INTEGER || (2**53 - 1)`; higher value
@@ -529,6 +550,10 @@ tuple.  For example, `1.2.3.4` will return `2.3.4` in rtl mode, not
`4.0.0`.  `1.2.3/4` will return `4.0.0`, because the `4` is not a part of
any other overlapping SemVer tuple.

+If the `options.includePrerelease` flag is set, then the `coerce` result will contain
+prerelease and build parts of a version.  For example, `1.2.3.4-rc.1+rev.2`
+will preserve prerelease `rc.1` and build `rev.2` in the result.
+
### Clean

* `clean(version)`: Clean a string to be a valid semver if possible
@@ -543,7 +568,7 @@ ex.
* `s.clean(' = v 2.1.5-foo')`: `null`
* `s.clean(' = v 2.1.5-foo', { loose: true })`: `'2.1.5-foo'`
* `s.clean('=v2.1.5')`: `'2.1.5'`
-* `s.clean('  =v2.1.5')`: `2.1.5`
+* `s.clean('  =v2.1.5')`: `'2.1.5'`
* `s.clean('      2.1.5   ')`: `'2.1.5'`
* `s.clean('~1.0.0')`: `null`

@@ -589,7 +614,7 @@ eg), and then pull the module name into the documentation for that specific
thing.
-->

-You may pull in just the part of this semver utility that you need, if you
+You may pull in just the part of this semver utility that you need if you
are sensitive to packing and tree-shaking concerns.  The main
`require('semver')` export uses getter functions to lazily load the parts
of the API that are used.
@@ -632,6 +657,8 @@ The following modules are available:
* `require('semver/ranges/min-satisfying')`
* `require('semver/ranges/min-version')`
* `require('semver/ranges/outside')`
+* `require('semver/ranges/simplify')`
+* `require('semver/ranges/subset')`
* `require('semver/ranges/to-comparators')`
* `require('semver/ranges/valid')`

diff --git SECURITY.md SECURITY.md
index 9cd2deaf..4fe06a2a 100644
--- SECURITY.md
+++ SECURITY.md
@@ -2,7 +2,7 @@

GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub).

-If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 
+If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways.

If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [[email protected]](mailto:[email protected]).

diff --git a/benchmarks/bench-compare.js b/benchmarks/bench-compare.js
new file mode 100644
index 00000000..2dfc0900
--- /dev/null
+++ benchmarks/bench-compare.js
@@ -0,0 +1,48 @@
+const Benchmark = require('benchmark')
+const SemVer = require('../classes/semver')
+const suite = new Benchmark.Suite()
+
+const versions = ['1.0.3', '2.2.2', '2.3.0']
+const versionToCompare = '1.0.2'
+const option1 = { includePrelease: true }
+const option2 = { includePrelease: true, loose: true }
+const option3 = { includePrelease: true, loose: true, rtl: true }
+
+for (const version of versions) {
+  suite.add(`compare ${version} to ${versionToCompare}`, function () {
+    const semver = new SemVer(version)
+    semver.compare(versionToCompare)
+  })
+}
+
+for (const version of versions) {
+  suite.add(
+    `compare ${version} to ${versionToCompare} with option (${JSON.stringify(option1)})`,
+    function () {
+      const semver = new SemVer(version, option1)
+      semver.compare(versionToCompare)
+    })
+}
+
+for (const version of versions) {
+  suite.add(`compare ${version} to ${versionToCompare} with option (${JSON.stringify(option2)})`,
+    function () {
+      const semver = new SemVer(version, option2)
+      semver.compare(versionToCompare)
+    })
+}
+
+for (const version of versions) {
+  suite.add(
+    `compare ${version} to ${versionToCompare} with option (${JSON.stringify(option3)})`,
+    function () {
+      const semver = new SemVer(version, option3)
+      semver.compare(versionToCompare)
+    })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-diff.js b/benchmarks/bench-diff.js
new file mode 100644
index 00000000..97d74441
--- /dev/null
+++ benchmarks/bench-diff.js
@@ -0,0 +1,21 @@
+const Benchmark = require('benchmark')
+const diff = require('../functions/diff')
+const suite = new Benchmark.Suite()
+
+const cases = [
+  ['0.0.1', '0.0.1-pre', 'patch'],
+  ['0.0.1', '0.0.1-pre-2', 'patch'],
+  ['1.1.0', '1.1.0-pre', 'minor'],
+]
+
+for (const [v1, v2] of cases) {
+  suite.add(`diff(${v1}, ${v2})`, function () {
+    diff(v1, v2)
+  })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-parse-options.js b/benchmarks/bench-parse-options.js
new file mode 100644
index 00000000..41ab232c
--- /dev/null
+++ benchmarks/bench-parse-options.js
@@ -0,0 +1,33 @@
+const Benchmark = require('benchmark')
+const parseOptions = require('../internal/parse-options')
+const suite = new Benchmark.Suite()
+
+const options1 = {
+  includePrerelease: true,
+}
+
+const options2 = {
+  includePrerelease: true,
+  loose: true,
+}
+
+const options3 = {
+  includePrerelease: true,
+  loose: true,
+  rtl: false,
+}
+
+suite
+  .add('includePrerelease', function () {
+    parseOptions(options1)
+  })
+  .add('includePrerelease + loose', function () {
+    parseOptions(options2)
+  })
+  .add('includePrerelease + loose + rtl', function () {
+    parseOptions(options3)
+  })
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-parse.js b/benchmarks/bench-parse.js
new file mode 100644
index 00000000..e4180c44
--- /dev/null
+++ benchmarks/bench-parse.js
@@ -0,0 +1,25 @@
+const Benchmark = require('benchmark')
+const parse = require('../functions/parse')
+const { MAX_SAFE_INTEGER } = require('../internal/constants')
+const suite = new Benchmark.Suite()
+
+const cases = ['1.2.1', '1.2.2-4', '1.2.3-pre']
+const invalidCases = [`${MAX_SAFE_INTEGER}0.0.0`, 'hello, world', 'xyz']
+
+for (const test of cases) {
+  suite.add(`parse(${test})`, function () {
+    parse(test)
+  })
+}
+
+for (const test of invalidCases) {
+  suite.add(`invalid parse(${test})`, function () {
+    parse(test)
+  })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-satisfies.js b/benchmarks/bench-satisfies.js
new file mode 100644
index 00000000..a95a2c30
--- /dev/null
+++ benchmarks/bench-satisfies.js
@@ -0,0 +1,39 @@
+const Benchmark = require('benchmark')
+const satisfies = require('../functions/satisfies')
+const suite = new Benchmark.Suite()
+
+const versions = ['1.0.3||^2.0.0', '2.2.2||~3.0.0', '2.3.0||<4.0.0']
+const versionToCompare = '1.0.6'
+const option1 = { includePrelease: true }
+const option2 = { includePrelease: true, loose: true }
+const option3 = { includePrelease: true, loose: true, rtl: true }
+
+for (const version of versions) {
+  suite.add(`satisfies(${versionToCompare}, ${version})`, function () {
+    satisfies(versionToCompare, version)
+  })
+}
+
+for (const version of versions) {
+  suite.add(`satisfies(${versionToCompare}, ${version}, ${JSON.stringify(option1)})`, function () {
+    satisfies(versionToCompare, version, option1)
+  })
+}
+
+for (const version of versions) {
+  suite.add(`satisfies(${versionToCompare}, ${version}, ${JSON.stringify(option2)})`, function () {
+    satisfies(versionToCompare, version, option2)
+  })
+}
+
+for (const version of versions) {
+  suite.add(`satisfies(${versionToCompare}, ${version}, ${JSON.stringify(option3)})`, function () {
+    satisfies(versionToCompare, version, option3)
+  })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git a/benchmarks/bench-subset.js b/benchmarks/bench-subset.js
new file mode 100644
index 00000000..f8825fae
--- /dev/null
+++ benchmarks/bench-subset.js
@@ -0,0 +1,25 @@
+const Benchmark = require('benchmark')
+const subset = require('../ranges/subset')
+const suite = new Benchmark.Suite()
+
+// taken from tests
+const cases = [
+  // everything is a subset of *
+  ['1.2.3', '*', true],
+  ['^1.2.3', '*', true],
+  ['^1.2.3-pre.0', '*', false],
+  ['^1.2.3-pre.0', '*', true, { includePrerelease: true }],
+  ['1 || 2 || 3', '*', true],
+]
+
+for (const [sub, dom] of cases) {
+  suite.add(`subset(${sub}, ${dom})`, function () {
+    subset(sub, dom)
+  })
+}
+
+suite
+  .on('cycle', function (event) {
+    console.log(String(event.target))
+  })
+  .run({ async: false })
diff --git bin/semver.js bin/semver.js
index 242b7ade..22fc76ea 100755
--- bin/semver.js
+++ bin/semver.js
@@ -61,6 +61,7 @@ const main = () => {
        switch (argv[0]) {
          case 'major': case 'minor': case 'patch': case 'prerelease':
          case 'premajor': case 'preminor': case 'prepatch':
+          case 'release':
            inc = argv.shift()
            break
          default:
@@ -119,7 +120,11 @@ const main = () => {
      return fail()
    }
  }
-  return success(versions)
+  versions
+    .sort((a, b) => semver[reverse ? 'rcompare' : 'compare'](a, b, options))
+    .map(v => semver.clean(v, options))
+    .map(v => inc ? semver.inc(v, inc, options, identifier, identifierBase) : v)
+    .forEach(v => console.log(v))
}

const failInc = () => {
@@ -129,19 +134,6 @@ const failInc = () => {

const fail = () => process.exit(1)

-const success = () => {
-  const compare = reverse ? 'rcompare' : 'compare'
-  versions.sort((a, b) => {
-    return semver[compare](a, b, options)
-  }).map((v) => {
-    return semver.clean(v, options)
-  }).map((v) => {
-    return inc ? semver.inc(v, inc, options, identifier, identifierBase) : v
-  }).forEach((v, i, _) => {
-    console.log(v)
-  })
-}
-
const help = () => console.log(
`SemVer ${version}

@@ -158,7 +150,7 @@ Options:
-i --increment [<level>]
        Increment a version by the specified level.  Level can
        be one of: major, minor, patch, premajor, preminor,
-        prepatch, or prerelease.  Default level is 'patch'.
+        prepatch, prerelease, or release.  Default level is 'patch'.
        Only one version may be specified.

--preid <identifier>
diff --git classes/range.js classes/range.js
index a7d37203..ceee2314 100644
--- classes/range.js
+++ classes/range.js
@@ -1,3 +1,5 @@
+const SPACE_CHARACTERS = /\s+/g
+
// hoisted class for cyclic dependency
class Range {
  constructor (range, options) {
@@ -18,7 +20,7 @@ class Range {
      // just put it in the set and return
      this.raw = range.value
      this.set = [[range]]
-      this.format()
+      this.formatted = undefined
      return this
    }

@@ -29,16 +31,13 @@ class Range {
    // First reduce all whitespace as much as possible so we do not have to rely
    // on potentially slow regexes like \s*. This is then stored and used for
    // future error messages as well.
-    this.raw = range
-      .trim()
-      .split(/\s+/)
-      .join(' ')
+    this.raw = range.trim().replace(SPACE_CHARACTERS, ' ')

    // First, split on ||
    this.set = this.raw
      .split('||')
      // map the range to a 2d array of comparators
-      .map(r => this.parseRange(r))
+      .map(r => this.parseRange(r.trim()))
      // throw out any comparator lists that are empty
      // this generally means that it was not a valid range, which is allowed
      // in loose mode, but will still throw if the WHOLE range is invalid.
@@ -66,14 +65,29 @@ class Range {
      }
    }

-    this.format()
+    this.formatted = undefined
+  }
+
+  get range () {
+    if (this.formatted === undefined) {
+      this.formatted = ''
+      for (let i = 0; i < this.set.length; i++) {
+        if (i > 0) {
+          this.formatted += '||'
+        }
+        const comps = this.set[i]
+        for (let k = 0; k < comps.length; k++) {
+          if (k > 0) {
+            this.formatted += ' '
+          }
+          this.formatted += comps[k].toString().trim()
+        }
+      }
+    }
+    return this.formatted
  }

  format () {
-    this.range = this.set
-      .map((comps) => comps.join(' ').trim())
-      .join('||')
-      .trim()
    return this.range
  }

@@ -198,8 +212,8 @@ class Range {

module.exports = Range

-const LRU = require('lru-cache')
-const cache = new LRU({ max: 1000 })
+const LRU = require('../internal/lrucache')
+const cache = new LRU()

const parseOptions = require('../internal/parse-options')
const Comparator = require('./comparator')
@@ -470,9 +484,10 @@ const replaceGTE0 = (comp, options) => {
// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5
// 1.2.3 - 3.4 => >=1.2.0 <3.5.0-0 Any 3.4.x will do
// 1.2 - 3.4 => >=1.2.0 <3.5.0-0
+// TODO build?
const hyphenReplace = incPr => ($0,
  from, fM, fm, fp, fpr, fb,
-  to, tM, tm, tp, tpr, tb) => {
+  to, tM, tm, tp, tpr) => {
  if (isX(fM)) {
    from = ''
  } else if (isX(fm)) {
diff --git classes/semver.js classes/semver.js
index 84e84590..6fbc062b 100644
--- classes/semver.js
+++ classes/semver.js
@@ -1,6 +1,6 @@
const debug = require('../internal/debug')
const { MAX_LENGTH, MAX_SAFE_INTEGER } = require('../internal/constants')
-const { safeRe: re, t } = require('../internal/re')
+const { safeRe: re, safeSrc: src, t } = require('../internal/re')

const parseOptions = require('../internal/parse-options')
const { compareIdentifiers } = require('../internal/identifiers')
@@ -10,7 +10,7 @@ class SemVer {

    if (version instanceof SemVer) {
      if (version.loose === !!options.loose &&
-          version.includePrerelease === !!options.includePrerelease) {
+        version.includePrerelease === !!options.includePrerelease) {
        return version
      } else {
        version = version.version
@@ -158,7 +158,7 @@ class SemVer {
    do {
      const a = this.build[i]
      const b = other.build[i]
-      debug('prerelease compare', i, a, b)
+      debug('build compare', i, a, b)
      if (a === undefined && b === undefined) {
        return 0
      } else if (b === undefined) {
@@ -176,6 +176,20 @@ class SemVer {
  // preminor will bump the version up to the next minor release, and immediately
  // down to pre-release. premajor and prepatch work the same way.
  inc (release, identifier, identifierBase) {
+    if (release.startsWith('pre')) {
+      if (!identifier && identifierBase === false) {
+        throw new Error('invalid increment argument: identifier is empty')
+      }
+      // Avoid an invalid semver results
+      if (identifier) {
+        const r = new RegExp(`^${this.options.loose ? src[t.PRERELEASELOOSE] : src[t.PRERELEASE]}$`)
+        const match = `-${identifier}`.match(r)
+        if (!match || match[1] !== identifier) {
+          throw new Error(`invalid identifier: ${identifier}`)
+        }
+      }
+    }
+
    switch (release) {
      case 'premajor':
        this.prerelease.length = 0
@@ -206,6 +220,12 @@ class SemVer {
        }
        this.inc('pre', identifier, identifierBase)
        break
+      case 'release':
+        if (this.prerelease.length === 0) {
+          throw new Error(`version ${this.raw} is not a prerelease`)
+        }
+        this.prerelease.length = 0
+        break

      case 'major':
        // If this is a pre-major version, bump up to the same major version.
@@ -249,10 +269,6 @@ class SemVer {
      case 'pre': {
        const base = Number(identifierBase) ? 1 : 0

-        if (!identifier && identifierBase === false) {
-          throw new Error('invalid increment argument: identifier is empty')
-        }
-
        if (this.prerelease.length === 0) {
          this.prerelease = [base]
        } else {
diff --git functions/coerce.js functions/coerce.js
index febbff9c..b378dcea 100644
--- functions/coerce.js
+++ functions/coerce.js
@@ -19,34 +19,42 @@ const coerce = (version, options) => {

  let match = null
  if (!options.rtl) {
-    match = version.match(re[t.COERCE])
+    match = version.match(options.includePrerelease ? re[t.COERCEFULL] : re[t.COERCE])
  } else {
    // Find the right-most coercible string that does not share
    // a terminus with a more left-ward coercible string.
    // Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4'
+    // With includePrerelease option set, '1.2.3.4-rc' wants to coerce '2.3.4-rc', not '2.3.4'
    //
    // Walk through the string checking with a /g regexp
    // Manually set the index so as to pick up overlapping matches.
    // Stop when we get a match that ends at the string end, since no
    // coercible string can be more right-ward without the same terminus.
+    const coerceRtlRegex = options.includePrerelease ? re[t.COERCERTLFULL] : re[t.COERCERTL]
    let next
-    while ((next = re[t.COERCERTL].exec(version)) &&
+    while ((next = coerceRtlRegex.exec(version)) &&
        (!match || match.index + match[0].length !== version.length)
    ) {
      if (!match ||
            next.index + next[0].length !== match.index + match[0].length) {
        match = next
      }
-      re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length
+      coerceRtlRegex.lastIndex = next.index + next[1].length + next[2].length
    }
    // leave it in a clean state
-    re[t.COERCERTL].lastIndex = -1
+    coerceRtlRegex.lastIndex = -1
  }

  if (match === null) {
    return null
  }

-  return parse(`${match[2]}.${match[3] || '0'}.${match[4] || '0'}`, options)
+  const major = match[2]
+  const minor = match[3] || '0'
+  const patch = match[4] || '0'
+  const prerelease = options.includePrerelease && match[5] ? `-${match[5]}` : ''
+  const build = options.includePrerelease && match[6] ? `+${match[6]}` : ''
+
+  return parse(`${major}.${minor}.${patch}${prerelease}${build}`, options)
}
module.exports = coerce
diff --git functions/diff.js functions/diff.js
index fc224e30..33171dc1 100644
--- functions/diff.js
+++ functions/diff.js
@@ -27,20 +27,13 @@ const diff = (version1, version2) => {
      return 'major'
    }

-    // Otherwise it can be determined by checking the high version
-
-    if (highVersion.patch) {
-      // anything higher than a patch bump would result in the wrong version
+    // If the main part has no difference
+    if (lowVersion.compareMain(highVersion) === 0) {
+      if (lowVersion.minor && !lowVersion.patch) {
+        return 'minor'
+      }
      return 'patch'
    }
-
-    if (highVersion.minor) {
-      // anything higher than a minor bump would result in the wrong version
-      return 'minor'
-    }
-
-    // bumping major/minor/patch all have same result
-    return 'major'
  }

  // add the `pre` prefix if we are going to a prerelease version
diff --git a/internal/lrucache.js b/internal/lrucache.js
new file mode 100644
index 00000000..6d89ec94
--- /dev/null
+++ internal/lrucache.js
@@ -0,0 +1,40 @@
+class LRUCache {
+  constructor () {
+    this.max = 1000
+    this.map = new Map()
+  }
+
+  get (key) {
+    const value = this.map.get(key)
+    if (value === undefined) {
+      return undefined
+    } else {
+      // Remove the key from the map and add it to the end
+      this.map.delete(key)
+      this.map.set(key, value)
+      return value
+    }
+  }
+
+  delete (key) {
+    return this.map.delete(key)
+  }
+
+  set (key, value) {
+    const deleted = this.delete(key)
+
+    if (!deleted && value !== undefined) {
+      // If cache is full, delete the least recently used item
+      if (this.map.size >= this.max) {
+        const firstKey = this.map.keys().next().value
+        this.delete(firstKey)
+      }
+
+      this.map.set(key, value)
+    }
+
+    return this
+  }
+}
+
+module.exports = LRUCache
diff --git internal/re.js internal/re.js
index 9f5e36d5..2a956ba0 100644
--- internal/re.js
+++ internal/re.js
@@ -1,4 +1,8 @@
-const { MAX_SAFE_COMPONENT_LENGTH, MAX_SAFE_BUILD_LENGTH } = require('./constants')
+const {
+  MAX_SAFE_COMPONENT_LENGTH,
+  MAX_SAFE_BUILD_LENGTH,
+  MAX_LENGTH,
+} = require('./constants')
const debug = require('./debug')
exports = module.exports = {}

@@ -6,6 +10,7 @@ exports = module.exports = {}
const re = exports.re = []
const safeRe = exports.safeRe = []
const src = exports.src = []
+const safeSrc = exports.safeSrc = []
const t = exports.t = {}
let R = 0

@@ -19,7 +24,7 @@ const LETTERDASHNUMBER = '[a-zA-Z0-9-]'
// all input should have extra whitespace removed.
const safeRegexReplacements = [
  ['\\s', 1],
-  ['\\d', MAX_SAFE_COMPONENT_LENGTH],
+  ['\\d', MAX_LENGTH],
  [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH],
]

@@ -38,6 +43,7 @@ const createToken = (name, value, isGlobal) => {
  debug(name, index, value)
  t[name] = index
  src[index] = value
+  safeSrc[index] = safe
  re[index] = new RegExp(value, isGlobal ? 'g' : undefined)
  safeRe[index] = new RegExp(safe, isGlobal ? 'g' : undefined)
}
@@ -150,12 +156,17 @@ createToken('XRANGELOOSE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`)

// Coercion.
// Extract anything that could conceivably be a part of a valid semver
-createToken('COERCE', `${'(^|[^\\d])' +
+createToken('COERCEPLAIN', `${'(^|[^\\d])' +
              '(\\d{1,'}${MAX_SAFE_COMPONENT_LENGTH}})` +
              `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` +
-              `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` +
+              `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?`)
+createToken('COERCE', `${src[t.COERCEPLAIN]}(?:$|[^\\d])`)
+createToken('COERCEFULL', src[t.COERCEPLAIN] +
+              `(?:${src[t.PRERELEASE]})?` +
+              `(?:${src[t.BUILD]})?` +
              `(?:$|[^\\d])`)
createToken('COERCERTL', src[t.COERCE], true)
+createToken('COERCERTLFULL', src[t.COERCEFULL], true)

// Tilde ranges.
// Meaning is "reasonably at or greater than"
diff --git package.json package.json
index 378164a7..c2644547 100644
--- package.json
+++ package.json
@@ -1,26 +1,28 @@
{
  "name": "semver",
-  "version": "7.5.3",
+  "version": "7.7.1",
  "description": "The semantic version parser used by npm.",
  "main": "index.js",
  "scripts": {
    "test": "tap",
    "snap": "tap",
-    "lint": "eslint \"**/*.js\"",
+    "lint": "npm run eslint",
    "postlint": "template-oss-check",
-    "lintfix": "npm run lint -- --fix",
+    "lintfix": "npm run eslint -- --fix",
    "posttest": "npm run lint",
-    "template-oss-apply": "template-oss-apply --force"
+    "template-oss-apply": "template-oss-apply --force",
+    "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\""
  },
  "devDependencies": {
-    "@npmcli/eslint-config": "^4.0.0",
-    "@npmcli/template-oss": "4.15.1",
+    "@npmcli/eslint-config": "^5.0.0",
+    "@npmcli/template-oss": "4.23.4",
+    "benchmark": "^2.1.4",
    "tap": "^16.0.0"
  },
  "license": "ISC",
  "repository": {
    "type": "git",
-    "url": "https://github.com/npm/node-semver.git"
+    "url": "git+https://github.com/npm/node-semver.git"
  },
  "bin": {
    "semver": "bin/semver.js"
@@ -47,23 +49,11 @@
  "engines": {
    "node": ">=10"
  },
-  "dependencies": {
-    "lru-cache": "^6.0.0"
-  },
  "author": "GitHub Inc.",
  "templateOSS": {
    "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
-    "version": "4.15.1",
+    "version": "4.23.4",
    "engines": ">=10",
-    "ciVersions": [
-      "10.0.0",
-      "10.x",
-      "12.x",
-      "14.x",
-      "16.x",
-      "18.x"
-    ],
-    "npmSpec": "8",
    "distPaths": [
      "classes/",
      "functions/",
@@ -80,7 +70,8 @@
      "/ranges/",
      "/index.js",
      "/preload.js",
-      "/range.bnf"
+      "/range.bnf",
+      "/benchmarks"
    ],
    "publish": "true"
  }
diff --git release-please-config.json release-please-config.json
index 73d1e353..a1676b9c 100644
--- release-please-config.json
+++ release-please-config.json
@@ -1,5 +1,4 @@
{
-  "exclude-packages-from-root": true,
  "group-pull-request-title-pattern": "chore: release ${version}",
  "pull-request-title-pattern": "chore: release${component} ${version}",
  "changelog-sections": [
@@ -25,6 +24,7 @@
    },
    {
      "type": "chore",
+      "section": "Chores",
      "hidden": true
    }
  ],
@@ -32,5 +32,6 @@
    ".": {
      "package-name": ""
    }
-  }
+  },
+  "prerelease-type": "pre"
}
diff --git tap-snapshots/test/bin/semver.js.test.cjs tap-snapshots/test/bin/semver.js.test.cjs
index e820ca47..4938f10d 100644
--- tap-snapshots/test/bin/semver.js.test.cjs
+++ tap-snapshots/test/bin/semver.js.test.cjs
@@ -70,7 +70,7 @@ Object {
    -i --increment [<level>]
            Increment a version by the specified level.  Level can
            be one of: major, minor, patch, premajor, preminor,
-            prepatch, or prerelease.  Default level is 'patch'.
+            prepatch, prerelease, or release.  Default level is 'patch'.
            Only one version may be specified.
    
    --preid <identifier>
@@ -131,7 +131,7 @@ Object {
    -i --increment [<level>]
            Increment a version by the specified level.  Level can
            be one of: major, minor, patch, premajor, preminor,
-            prepatch, or prerelease.  Default level is 'patch'.
+            prepatch, prerelease, or release.  Default level is 'patch'.
            Only one version may be specified.
    
    --preid <identifier>
@@ -192,7 +192,7 @@ Object {
    -i --increment [<level>]
            Increment a version by the specified level.  Level can
            be one of: major, minor, patch, premajor, preminor,
-            prepatch, or prerelease.  Default level is 'patch'.
+            prepatch, prerelease, or release.  Default level is 'patch'.
            Only one version may be specified.
    
    --preid <identifier>
@@ -253,7 +253,7 @@ Object {
    -i --increment [<level>]
            Increment a version by the specified level.  Level can
            be one of: major, minor, patch, premajor, preminor,
-            prepatch, or prerelease.  Default level is 'patch'.
+            prepatch, prerelease, or release.  Default level is 'patch'.
            Only one version may be specified.
    
    --preid <identifier>
@@ -348,6 +348,15 @@ Object {
}
`

+exports[`test/bin/semver.js TAP inc tests > -i release 1.0.0-pre`] = `
+Object {
+  "code": 0,
+  "err": "",
+  "out": "1.0.0\\n",
+  "signal": null,
+}
+`
+
exports[`test/bin/semver.js TAP sorting and filtering > 1.2.3 -v 3.2.1 --version 2.3.4 -rv 1`] = `
Object {
  "code": 0,
diff --git test/classes/range.js test/classes/range.js
index 24686adf..c1e6eb1b 100644
--- test/classes/range.js
+++ test/classes/range.js
@@ -82,6 +82,15 @@ test('tostrings', (t) => {
  t.end()
})

+test('formatted value is calculated lazily and cached', (t) => {
+  const r = new Range('>= v1.2.3')
+  t.equal(r.formatted, undefined)
+  t.equal(r.format(), '>=1.2.3')
+  t.equal(r.formatted, '>=1.2.3')
+  t.equal(r.format(), '>=1.2.3')
+  t.end()
+})
+
test('ranges intersect', (t) => {
  rangeIntersection.forEach(([r0, r1, expect]) => {
    t.test(`${r0} <~> ${r1}`, t => {
@@ -105,3 +114,13 @@ test('missing range parameter in range intersect', (t) => {
  'throws type error')
  t.end()
})
+
+test('cache', (t) => {
+  const cached = Symbol('cached')
+  const r1 = new Range('1.0.0')
+  r1.set[0][cached] = true
+  const r2 = new Range('1.0.0')
+  t.equal(r1.set[0][cached], true)
+  t.equal(r2.set[0][cached], true) // Will be true, showing it's cached.
+  t.end()
+})
diff --git test/classes/semver.js test/classes/semver.js
index 1e4d48f8..946fa417 100644
--- test/classes/semver.js
+++ test/classes/semver.js
@@ -106,6 +106,34 @@ test('incrementing', t => {
  }))
})

+test('invalid increments', (t) => {
+  t.throws(
+    () => new SemVer('1.2.3').inc('prerelease', '', false),
+    Error('invalid increment argument: identifier is empty')
+  )
+  t.throws(
+    () => new SemVer('1.2.3-dev').inc('prerelease', 'dev', false),
+    Error('invalid increment argument: identifier already exists')
+  )
+  t.throws(
+    () => new SemVer('1.2.3').inc('prerelease', 'invalid/preid'),
+    Error('invalid identifier: invalid/preid')
+  )
+
+  t.end()
+})
+
+test('increment side-effects', (t) => {
+  const v = new SemVer('1.0.0')
+  try {
+    v.inc('prerelease', 'hot/mess')
+  } catch (er) {
+    // ignore but check that the version has not changed
+  }
+  t.equal(v.toString(), '1.0.0')
+  t.end()
+})
+
test('compare main vs pre', (t) => {
  const s = new SemVer('1.2.3')
  t.equal(s.compareMain('2.3.4'), -1)
@@ -123,25 +151,6 @@ test('compare main vs pre', (t) => {
  t.end()
})

-test('invalid version numbers', (t) => {
-  ['1.2.3.4', 'NOT VALID', 1.2, null, 'Infinity.NaN.Infinity'].forEach((v) => {
-    t.throws(
-      () => {
-        new SemVer(v) // eslint-disable-line no-new
-      },
-      {
-        name: 'TypeError',
-        message:
-          typeof v === 'string'
-            ? `Invalid Version: ${v}`
-            : `Invalid version. Must be a string. Got type "${typeof v}".`,
-      }
-    )
-  })
-
-  t.end()
-})
-
test('compareBuild', (t) => {
  const noBuild = new SemVer('1.0.0')
  const build0 = new SemVer('1.0.0+0')
diff --git test/fixtures/increments.js test/fixtures/increments.js
index 65e9530b..f0839df5 100644
--- test/fixtures/increments.js
+++ test/fixtures/increments.js
@@ -32,6 +32,7 @@ module.exports = [
  ['1.2.3-alpha.9.beta', 'prerelease', '1.2.3-alpha.10.beta'],
  ['1.2.3-alpha.10.beta', 'prerelease', '1.2.3-alpha.11.beta'],
  ['1.2.3-alpha.11.beta', 'prerelease', '1.2.3-alpha.12.beta'],
+  ['1.0.0', 'prepatch', '1.0.1-alpha.1.1a.0', null, 'alpha.1.1a'],
  ['1.2.0', 'prepatch', '1.2.1-0'],
  ['1.2.0-1', 'prepatch', '1.2.1-0'],
  ['1.2.0', 'preminor', '1.3.0-0'],
@@ -40,7 +41,12 @@ module.exports = [
  ['1.2.3-1', 'premajor', '2.0.0-0'],
  ['1.2.0-1', 'minor', '1.2.0'],
  ['1.0.0-1', 'major', '1.0.0'],
+  ['1.0.0-1', 'release', '1.0.0'],
+  ['1.2.0-1', 'release', '1.2.0'],
+  ['1.2.3-1', 'release', '1.2.3'],
+  ['1.2.3', 'release', null],

+  // [version, inc, result, identifierIndex, loose, identifier]
  ['1.2.3', 'major', '2.0.0', false, 'dev'],
  ['1.2.3', 'minor', '1.3.0', false, 'dev'],
  ['1.2.3', 'patch', '1.2.4', false, 'dev'],
@@ -88,7 +94,7 @@ module.exports = [
  ['1.2.3-1.1', 'prerelease', '1.2.3-1.2', false, '1'],
  ['1.2.3-1.1', 'prerelease', '1.2.3-2.0', false, '2'],

-  // [version, inc, result, identifierIndex, loose, identifier]
+  // [version, inc, result, identifierIndex, loose, identifier, identifierBase]
  ['1.2.0-1', 'prerelease', '1.2.0-alpha.0', false, 'alpha', '0'],
  ['1.2.1', 'prerelease', '1.2.2-alpha.0', false, 'alpha', '0'],
  ['0.2.0', 'prerelease', '0.2.1-alpha.0', false, 'alpha', '0'],
@@ -124,4 +130,7 @@ module.exports = [
  ['1.2.0-dev', 'prepatch', '1.2.1-dev', false, 'dev', false],
  ['1.2.0', 'prerelease', null, false, '', false],
  ['1.0.0-rc.1+build.4', 'prerelease', '1.0.0-rc.2', 'rc', false],
+  ['1.2.0', 'prerelease', null, false, 'invalid/preid'],
+  ['1.2.0', 'prerelease', null, false, 'invalid+build'],
+  ['1.2.0beta', 'prerelease', null, { loose: true }, 'invalid/preid'],
]
diff --git test/fixtures/range-exclude.js test/fixtures/range-exclude.js
index 4b6c5631..2789148a 100644
--- test/fixtures/range-exclude.js
+++ test/fixtures/range-exclude.js
@@ -102,4 +102,6 @@ module.exports = [
  ['>=1.0.0 <1.1.0', '1.1.0', { includePrerelease: true }],
  ['>=1.0.0 <1.1.0', '1.1.0-pre'],
  ['>=1.0.0 <1.1.0-pre', '1.1.0-pre'],
+
+  ['== 1.0.0 || foo', '2.0.0', { loose: true }],
]
diff --git test/functions/clean.js test/functions/clean.js
index 1df155bf..830e824b 100644
--- test/functions/clean.js
+++ test/functions/clean.js
@@ -17,6 +17,7 @@ test('clean tests', (t) => {
    ['~1.2.3', null],
    ['<=1.2.3', null],
    ['1.2.x', null],
+    ['0.12.0-dev.1150+3c22cecee', '0.12.0-dev.1150'],
  ].forEach(([range, version]) => {
    const msg = `clean(${range}) = ${version}`
    t.equal(clean(range), version, msg)
diff --git test/functions/coerce.js test/functions/coerce.js
index ad9f199f..24e2ff76 100644
--- test/functions/coerce.js
+++ test/functions/coerce.js
@@ -110,13 +110,47 @@ test('coerce tests', (t) => {
    ['1.2.3/6', '6.0.0', { rtl: true }],
    ['1.2.3.4', '2.3.4', { rtl: true }],
    ['1.2.3.4xyz', '2.3.4', { rtl: true }],
+
+    ['1-rc.5', '1.0.0-rc.5', { includePrerelease: true }],
+    ['1.2-rc.5', '1.2.0-rc.5', { includePrerelease: true }],
+    ['1.2.3-rc.5', '1.2.3-rc.5', { includePrerelease: true }],
+    ['1.2.3-rc.5/a', '1.2.3-rc.5', { includePrerelease: true }],
+    ['1.2.3.4-rc.5', '1.2.3', { includePrerelease: true }],
+    ['1.2.3.4+rev.6', '1.2.3', { includePrerelease: true }],
+
+    ['1+rev.6', '1.0.0+rev.6', { includePrerelease: true }],
+    ['1.2+rev.6', '1.2.0+rev.6', { includePrerelease: true }],
+    ['1.2.3+rev.6', '1.2.3+rev.6', { includePrerelease: true }],
+    ['1.2.3+rev.6/a', '1.2.3+rev.6', { includePrerelease: true }],
+    ['1.2.3.4-rc.5', '1.2.3', { includePrerelease: true }],
+    ['1.2.3.4+rev.6', '1.2.3', { includePrerelease: true }],
+
+    ['1-rc.5+rev.6', '1.0.0-rc.5+rev.6', { includePrerelease: true }],
+    ['1.2-rc.5+rev.6', '1.2.0-rc.5+rev.6', { includePrerelease: true }],
+    ['1.2.3-rc.5+rev.6', '1.2.3-rc.5+rev.6', { includePrerelease: true }],
+    ['1.2.3-rc.5+rev.6/a', '1.2.3-rc.5+rev.6', { includePrerelease: true }],
+
+    ['1.2-rc.5+rev.6', '1.2.0-rc.5+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3-rc.5+rev.6', '1.2.3-rc.5+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4-rc.5+rev.6', '2.3.4-rc.5+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4-rc.5', '2.3.4-rc.5', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4+rev.6', '2.3.4+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4-rc.5+rev.6/7', '7.0.0', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4-rc/7.5+rev.6', '7.5.0+rev.6', { rtl: true, includePrerelease: true }],
+    ['1.2.3.4/7-rc.5+rev.6', '7.0.0-rc.5+rev.6', { rtl: true, includePrerelease: true }],
  ]
  coerceToValid.forEach(([input, expected, options]) => {
-    const msg = `coerce(${input}) should become ${expected}`
-    t.same((coerce(input, options) || {}).version, expected, msg)
+    const coerceExpression = `coerce(${input}, ${JSON.stringify(options)})`
+    const coercedVersion = coerce(input, options) || {}
+    const expectedVersion = parse(expected)
+    t.equal(expectedVersion.compare(coercedVersion), 0,
+      `${coerceExpression} should be equal to ${expectedVersion}`)
+    t.equal(expectedVersion.compareBuild(coercedVersion), 0,
+      `${coerceExpression} build should be equal to ${expectedVersion}`)
  })

  t.same(valid(coerce('42.6.7.9.3-alpha')), '42.6.7')
+  t.same(valid(coerce('42.6.7-alpha+rev.1', { includePrerelease: true })), '42.6.7-alpha')
  t.same(valid(coerce('v2')), '2.0.0')

  t.end()
diff --git test/functions/diff.js test/functions/diff.js
index 720e159b..80f5e3c3 100644
--- test/functions/diff.js
+++ test/functions/diff.js
@@ -34,6 +34,13 @@ test('diff versions test', (t) => {
    ['1.0.0-1', '2.0.0-1', 'premajor'],
    ['1.0.0-1', '1.1.0-1', 'preminor'],
    ['1.0.0-1', '1.0.1-1', 'prepatch'],
+    ['1.7.2-1', '1.8.1', 'minor'],
+    ['1.1.1-pre', '2.1.1-pre', 'premajor'],
+    ['1.1.1-pre', '2.1.1', 'major'],
+    ['1.2.3-1', '1.2.3', 'patch'],
+    ['1.4.0-1', '2.3.5', 'major'],
+    ['1.6.1-5', '1.7.2', 'minor'],
+    ['2.0.0-1', '2.1.1', 'major'],
  ].forEach((v) => {
    const version1 = v[0]
    const version2 = v[1]
diff --git test/functions/valid.js test/functions/valid.js
index ab51fed3..33399ed7 100644
--- test/functions/valid.js
+++ test/functions/valid.js
@@ -2,6 +2,7 @@ const t = require('tap')
const valid = require('../../functions/valid')
const SemVer = require('../../classes/semver')
const invalidVersions = require('../fixtures/invalid-versions')
+const { MAX_SAFE_INTEGER } = require('../../internal/constants')

t.test('returns null instead of throwing when presented with garbage', t => {
  t.plan(invalidVersions.length)
@@ -17,3 +18,12 @@ t.test('validate a version into a SemVer object', t => {
  t.equal(valid('4.2.0foo', { loose: true }), '4.2.0-foo', 'looseness as an option')
  t.end()
})
+
+t.test('long build id', t => {
+  const longBuild = '-928490632884417731e7af463c92b034d6a78268fc993bcb88a57944'
+  const shortVersion = '1.1.1'
+  const longVersion = `${MAX_SAFE_INTEGER}.${MAX_SAFE_INTEGER}.${MAX_SAFE_INTEGER}`
+  t.equal(valid(shortVersion + longBuild), shortVersion + longBuild)
+  t.equal(valid(longVersion + longBuild), longVersion + longBuild)
+  t.end()
+})
diff --git test/integration/whitespace.js test/integration/whitespace.js
index ae1451b1..a3541325 100644
--- test/integration/whitespace.js
+++ test/integration/whitespace.js
@@ -29,8 +29,8 @@ test('range with 0', (t) => {
  t.throws(() => new Range(r).range)
  t.equal(validRange(r), null)
  t.throws(() => minVersion(r).version)
-  t.equal(minSatisfying(['1.2.3']), null)
-  t.equal(maxSatisfying(['1.2.3']), null)
+  t.equal(minSatisfying(['1.2.3'], r), null)
+  t.equal(maxSatisfying(['1.2.3'], r), null)
  t.end()
})

diff --git a/test/internal/lrucache.js b/test/internal/lrucache.js
new file mode 100644
index 00000000..83a0e797
--- /dev/null
+++ test/internal/lrucache.js
@@ -0,0 +1,19 @@
+const { test } = require('tap')
+const LRUCache = require('../../internal/lrucache')
+
+test('basic cache operation', t => {
+  const c = new LRUCache()
+  const max = 1000
+
+  for (let i = 0; i < max; i++) {
+    t.equal(c.set(i, i), c)
+  }
+  for (let i = 0; i < max; i++) {
+    t.equal(c.get(i), i)
+  }
+  c.set(1001, 1001)
+  // lru item should be gone
+  t.equal(c.get(0), undefined)
+  c.set(42, undefined)
+  t.end()
+})
diff --git test/internal/re.js test/internal/re.js
index 2851b325..1db0586a 100644
--- test/internal/re.js
+++ test/internal/re.js
@@ -1,15 +1,18 @@
const { test } = require('tap')
-const { src, re, safeRe } = require('../../internal/re')
+const { src, re, safeRe, safeSrc } = require('../../internal/re')
const semver = require('../../')

-test('has a list of src, re, and tokens', (t) => {
+test('Semver itself has a list of src, re, and tokens', (t) => {
  t.match(Object.assign({}, semver), {
-    src: Array,
    re: Array,
+    src: Array,
    tokens: Object,
  })
  re.forEach(r => t.match(r, RegExp, 'regexps are regexps'))
-  src.forEach(s => t.match(s, String, 'src is strings'))
+  safeRe.forEach(r => t.match(r, RegExp, 'safe regexps are regexps'))
+  src.forEach(s => t.match(s, String, 'src are strings'))
+  safeSrc.forEach(s => t.match(s, String, 'safe srcare strings'))
+  t.ok(Object.keys(semver.tokens).length, 'there are tokens')
  for (const i in semver.tokens) {
    t.match(semver.tokens[i], Number, 'tokens are numbers')
  }


Here's my review of the PR:

Description

This PR updates the node-semver package with numerous changes and improvements. Key updates include:

  • Removing dependency on lru-cache and implementing an internal cache
  • Adding new benchmarking functionality
  • Improving coercion behavior to handle prerelease and build metadata
  • Adding "release" increment type
  • Optimizing Range parsing and formatting
  • Updating CI workflows and GitHub settings

Possible Issues

  1. The change to coerce() function behavior with includePrerelease option could potentially break existing code that relies on the previous behavior
  2. The removal of the lru-cache dependency and switch to internal cache implementation may affect performance for edge cases

Security Hotspots

The removal of external dependency lru-cache reduces the attack surface and potential security vulnerabilities from third-party code.

Changes

Changes

By filename:

classes/range.js:

  • Optimized Range parsing and formatting
  • Added lazy evaluation of formatted value
  • Improved caching mechanism

classes/semver.js:

  • Added validation for prerelease identifiers
  • Added new "release" increment type
  • Improved error handling

functions/coerce.js:

  • Added support for preserving prerelease and build metadata when coercing versions
  • Improved RTL coercion behavior

internal/:

  • Added new internal LRU cache implementation to replace lru-cache dependency
  • Updated regex handling

package.json:

  • Removed lru-cache dependency
  • Updated dev dependencies
  • Added new benchmarking scripts
sequenceDiagram
    participant Client
    participant SemVer
    participant Range
    participant Cache

    Client->>SemVer: version operation
    SemVer->>Cache: check cache
    alt cache hit
        Cache-->>SemVer: return cached result
    else cache miss
        SemVer->>Range: parse version/range
        Range->>Range: lazy format value
        Range-->>SemVer: return parsed result
        SemVer->>Cache: store in cache
    end
    SemVer-->>Client: return result
Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0 participants