--- docs/case-studies/issue-9/current-release.yml	2025-12-25 01:13:46.311684867 +0100
+++ docs/case-studies/issue-9/template-release.yml	2025-12-25 01:13:46.314684844 +0100
@@ -6,6 +6,31 @@
       - main
   pull_request:
     types: [opened, synchronize, reopened]
+  # Manual release support - consolidated here to work with npm trusted publishing
+  # npm only allows ONE workflow file as trusted publisher, so all publishing
+  # must go through this workflow (release.yml)
+  workflow_dispatch:
+    inputs:
+      release_mode:
+        description: 'Manual release mode'
+        required: true
+        type: choice
+        default: 'instant'
+        options:
+          - instant
+          - changeset-pr
+      bump_type:
+        description: 'Manual release type'
+        required: true
+        type: choice
+        options:
+          - patch
+          - minor
+          - major
+      description:
+        description: 'Manual release description (optional)'
+        required: false
+        type: string
 
 concurrency: ${{ github.workflow }}-${{ github.ref }}
 
@@ -29,6 +54,11 @@
         run: npm install
 
       - name: Check for changesets
+        env:
+          # Pass PR context to the validation script
+          GITHUB_BASE_REF: ${{ github.base_ref }}
+          GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
+          GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
         run: |
           # Skip changeset check for automated version PRs
           if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then
@@ -37,6 +67,8 @@
           fi
 
           # Run changeset validation script
+          # This validates that exactly ONE changeset was ADDED by this PR
+          # Pre-existing changesets from other merged PRs are ignored
           node scripts/validate-changeset.mjs
 
   # Linting and formatting - runs after changeset check on PRs, immediately on main
@@ -62,8 +94,8 @@
       - name: Check formatting
         run: npm run format:check
 
-      - name: Check file size limit
-        run: npm run check:file-size
+      - name: Check code duplication
+        run: npm run check:duplication
 
   # Test matrix: 3 runtimes (Node.js, Bun, Deno) x 3 OS (Ubuntu, macOS, Windows)
   test:
@@ -115,7 +147,7 @@
 
       - name: Run tests (Deno)
         if: matrix.runtime == 'deno'
-        run: deno test --allow-read --allow-write --allow-env --allow-run
+        run: deno test --allow-read
 
   # Release - only runs on main after tests pass (for push events)
   release:
@@ -125,7 +157,7 @@
     # This is needed because lint/test jobs have a transitive dependency on changeset-check
     if: always() && github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.lint.result == 'success' && needs.test.result == 'success'
     runs-on: ubuntu-latest
-    # Permissions required for npm publishing and GitHub releases
+    # Permissions required for npm OIDC trusted publishing
     permissions:
       contents: write
       pull-requests: write
@@ -144,6 +176,9 @@
       - name: Install dependencies
         run: npm install
 
+      - name: Update npm for OIDC trusted publishing
+        run: node scripts/setup-npm.mjs
+
       - name: Check for changesets
         id: check_changesets
         run: |
@@ -151,110 +186,139 @@
           CHANGESET_COUNT=$(find .changeset -name "*.md" ! -name "README.md" | wc -l)
           echo "Found $CHANGESET_COUNT changeset file(s)"
           echo "has_changesets=$([[ $CHANGESET_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
+          echo "changeset_count=$CHANGESET_COUNT" >> $GITHUB_OUTPUT
+
+      - name: Merge multiple changesets
+        if: steps.check_changesets.outputs.has_changesets == 'true' && steps.check_changesets.outputs.changeset_count > 1
+        run: |
+          echo "Multiple changesets detected, merging..."
+          node scripts/merge-changesets.mjs
 
       - name: Version packages and commit to main
         if: steps.check_changesets.outputs.has_changesets == 'true'
         id: version
-        run: |
-          # Configure git
-          git config user.name "github-actions[bot]"
-          git config user.email "github-actions[bot]@users.noreply.github.com"
-
-          # Get current version before bump
-          OLD_VERSION=$(node -p "require('./package.json').version")
-          echo "Current version: $OLD_VERSION"
-
-          # Run changeset version to bump versions and update CHANGELOG
-          npm run changeset:version
-
-          # Get new version after bump
-          NEW_VERSION=$(node -p "require('./package.json').version")
-          echo "New version: $NEW_VERSION"
-          echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
-
-          # Check if there are changes to commit
-          if [[ -n $(git status --porcelain) ]]; then
-            echo "Changes detected, committing..."
-
-            # Stage all changes (package.json, package-lock.json, CHANGELOG.md, deleted changesets)
-            git add -A
-
-            # Commit with version number as message
-            git commit -m "$NEW_VERSION" \
-                       -m "" \
-                       -m "🤖 Generated with [Claude Code](https://claude.com/claude-code)"
-
-            # Push directly to main
-            git push origin main
-
-            echo "✅ Version bump committed and pushed to main"
-            echo "version_committed=true" >> $GITHUB_OUTPUT
-          else
-            echo "No changes to commit"
-            echo "version_committed=false" >> $GITHUB_OUTPUT
-          fi
+        run: node scripts/version-and-commit.mjs --mode changeset
 
       - name: Publish to npm
-        if: steps.version.outputs.version_committed == 'true'
+        # Run if version was committed OR if a previous attempt already committed (for re-runs)
+        if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
         id: publish
-        run: |
-          # Pull the latest changes we just pushed
-          git pull origin main
-
-          # Publish to npm
-          npm run changeset:publish
-
-          echo "published=true" >> $GITHUB_OUTPUT
+        run: node scripts/publish-to-npm.mjs --should-pull
 
-          # Get published version
-          PUBLISHED_VERSION=$(node -p "require('./package.json').version")
-          echo "published_version=$PUBLISHED_VERSION" >> $GITHUB_OUTPUT
-          echo "✅ Published gh-upload-log@$PUBLISHED_VERSION to npm"
+      - name: Create GitHub Release
+        if: steps.publish.outputs.published == 'true'
         env:
-          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
-          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}"
 
-      - name: Create GitHub Release
+      - name: Format GitHub release notes
         if: steps.publish.outputs.published == 'true'
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        run: |
-          VERSION="${{ steps.publish.outputs.published_version }}"
-          TAG="v$VERSION"
+        run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}"
 
-          echo "Creating GitHub release for $TAG..."
+  # Manual Instant Release - triggered via workflow_dispatch with instant mode
+  # This job is in release.yml because npm trusted publishing
+  # only allows one workflow file to be registered as a trusted publisher
+  instant-release:
+    name: Instant Release
+    if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant'
+    runs-on: ubuntu-latest
+    # Permissions required for npm OIDC trusted publishing
+    permissions:
+      contents: write
+      pull-requests: write
+      id-token: write
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
 
-          # Extract changelog entry for this version
-          # Read from CHANGELOG.md between this version and the next version marker
-          RELEASE_NOTES=$(awk "/## $VERSION/,/## [0-9]/" CHANGELOG.md | sed '1d;$d' | sed '/^$/d')
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20.x'
+          registry-url: 'https://registry.npmjs.org'
 
-          if [ -z "$RELEASE_NOTES" ]; then
-            RELEASE_NOTES="Release $VERSION"
-          fi
+      - name: Install dependencies
+        run: npm install
 
-          # Create release
-          gh release create "$TAG" \
-            --title "$VERSION" \
-            --notes "$RELEASE_NOTES" \
-            --repo ${{ github.repository }}
+      - name: Update npm for OIDC trusted publishing
+        run: node scripts/setup-npm.mjs
 
-          echo "✅ Created GitHub release: $TAG"
+      - name: Version packages and commit to main
+        id: version
+        run: node scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}"
+
+      - name: Publish to npm
+        # Run if version was committed OR if a previous attempt already committed (for re-runs)
+        if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
+        id: publish
+        run: node scripts/publish-to-npm.mjs
+
+      - name: Create GitHub Release
+        if: steps.publish.outputs.published == 'true'
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}"
 
       - name: Format GitHub release notes
         if: steps.publish.outputs.published == 'true'
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}"
+
+  # Manual Changeset PR - creates a pull request with the changeset for review
+  changeset-pr:
+    name: Create Changeset PR
+    if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changeset-pr'
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+      pull-requests: write
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20.x'
+
+      - name: Install dependencies
+        run: npm install
+
+      - name: Create changeset file
+        run: node scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}"
+
+      - name: Format changeset with Prettier
         run: |
-          VERSION="${{ steps.publish.outputs.published_version }}"
-          TAG="v$VERSION"
+          # Run Prettier on the changeset file to ensure it matches project style
+          npx prettier --write ".changeset/*.md" || true
 
-          # Get the release ID for this version
-          RELEASE_ID=$(gh api repos/${{ github.repository }}/releases/tags/$TAG --jq '.id' 2>/dev/null || echo "")
+          echo "Formatted changeset files"
 
-          if [ -n "$RELEASE_ID" ]; then
-            echo "Formatting release notes for $TAG..."
-            node scripts/format-release-notes.mjs "$RELEASE_ID" "$TAG" "${{ github.repository }}"
-            echo "✅ Formatted release notes for $TAG"
-          else
-            echo "⚠️ Could not find release for $TAG"
-          fi
-        env:
-          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - name: Create Pull Request
+        uses: peter-evans/create-pull-request@v7
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          commit-message: 'chore: add changeset for manual ${{ github.event.inputs.bump_type }} release'
+          branch: changeset-manual-release-${{ github.run_id }}
+          delete-branch: true
+          title: 'chore: manual ${{ github.event.inputs.bump_type }} release'
+          body: |
+            ## Manual Release Request
+
+            This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release.
+
+            ### Release Details
+            - **Type:** ${{ github.event.inputs.bump_type }}
+            - **Description:** ${{ github.event.inputs.description || 'Manual release' }}
+            - **Triggered by:** @${{ github.actor }}
+
+            ### Next Steps
+            1. Review the changeset in this PR
+            2. Merge this PR to main
+            3. The automated release workflow will create a version PR
+            4. Merge the version PR to publish to npm and create a GitHub release
