diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..62267a0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 10.8.0 + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test:ci + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/pr-changelog.yml b/.github/workflows/pr-changelog.yml new file mode 100644 index 0000000..2a6dc38 --- /dev/null +++ b/.github/workflows/pr-changelog.yml @@ -0,0 +1,73 @@ +name: PR Changelog + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + branches: [ main ] + +jobs: + validate-pr: + name: Validate PR Description + runs-on: ubuntu-latest + + steps: + - name: Check PR Description + id: check-pr + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const body = pr.body || ''; + + // Check if PR has a changelog section + const hasChangelog = body.includes('## Changelog') || + body.includes('## Changes') || + body.includes('## What Changed'); + + if (!hasChangelog) { + core.setFailed('PR description should include a changelog section (## Changelog, ## Changes, or ## What Changed)'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: '⚠️ Please add a changelog section to your PR description. This will be used in release notes.\n\nAdd one of these sections:\n- `## Changelog`\n- `## Changes`\n- `## What Changed`\n\nAnd describe the changes in a user-friendly way.' + }); + return; + } + + core.info('PR has a valid changelog section'); + + - name: Add Label + if: success() + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + + // Determine PR type based on title or labels + let prType = 'other'; + const title = pr.title.toLowerCase(); + + if (title.startsWith('fix:') || title.includes('bug') || title.includes('fix')) { + prType = 'fix'; + } else if (title.startsWith('feat:') || title.includes('feature')) { + prType = 'feature'; + } else if (title.includes('breaking') || title.includes('!:')) { + prType = 'breaking'; + } else if (title.startsWith('docs:') || title.includes('documentation')) { + prType = 'docs'; + } else if (title.startsWith('chore:') || title.includes('chore')) { + prType = 'chore'; + } + + // Add appropriate label + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: [`type: ${prType}`] + }); + } catch (error) { + core.warning(`Failed to add label: ${error.message}`); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e5adacf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: Release + +on: + release: + types: [published] + +jobs: + release: + name: Release + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 10.8.0 + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test:ci + + - name: Publish to NPM + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: pnpm publish --no-git-checks diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 0000000..cb55022 --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,143 @@ +name: Version Bump + +on: + push: + branches: [ main ] + +jobs: + version-bump: + name: Version Bump + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'skip ci') && !contains(github.event.head_commit.message, 'chore(release)')" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 10.8.0 + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test:ci + + - name: Test Release (Dry Run) + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} + run: pnpm release:dry-run + + - name: Update or Create GitHub Release Draft + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} + run: | + # Get the next version from semantic-release + VERSION=$(npx semantic-release --dry-run | grep -oP 'The next release version is \K[0-9]+\.[0-9]+\.[0-9]+' || echo "") + + if [ -z "$VERSION" ]; then + echo "No version change detected, skipping release creation" + exit 0 + fi + + echo "Next version will be: $VERSION" + + # Update package.json version + npm version $VERSION --no-git-tag-version + + # Generate changelog for this version + npx semantic-release --dry-run --no-ci > release-notes.md + + # Extract just the release notes section + sed -n '/# \[/,/^$/p' release-notes.md > changelog-extract.md + + # Get PR information + PR_NUMBER=$(echo "${{ github.event.head_commit.message }}" | grep -oP '#\K[0-9]+' || echo "") + PR_TITLE="" + PR_BODY="" + + if [ ! -z "$PR_NUMBER" ]; then + PR_INFO=$(gh pr view $PR_NUMBER --json title,body || echo "{}") + PR_TITLE=$(echo "$PR_INFO" | jq -r '.title // ""') + PR_BODY=$(echo "$PR_INFO" | jq -r '.body // ""') + fi + + # Check if draft release already exists + RELEASE_EXISTS=$(gh release view v$VERSION --json isDraft 2>/dev/null || echo "{}") + IS_DRAFT=$(echo "$RELEASE_EXISTS" | jq -r '.isDraft // false') + + if [ "$IS_DRAFT" = "true" ]; then + echo "Updating existing draft release v$VERSION" + + # Get existing release notes + gh release view v$VERSION --json body | jq -r '.body' > existing-notes.md + + # Add new PR information if available + if [ ! -z "$PR_NUMBER" ] && [ ! -z "$PR_TITLE" ]; then + echo -e "\n### PR #$PR_NUMBER: $PR_TITLE\n" >> existing-notes.md + if [ ! -z "$PR_BODY" ]; then + echo -e "$PR_BODY\n" >> existing-notes.md + fi + fi + + # Update the release + gh release edit v$VERSION --notes-file existing-notes.md + else + echo "Creating new draft release v$VERSION" + + # Create initial release notes + echo -e "# Release v$VERSION\n" > release-notes.md + cat changelog-extract.md >> release-notes.md + + # Add PR information if available + if [ ! -z "$PR_NUMBER" ] && [ ! -z "$PR_TITLE" ]; then + echo -e "\n## Pull Requests\n" >> release-notes.md + echo -e "### PR #$PR_NUMBER: $PR_TITLE\n" >> release-notes.md + if [ ! -z "$PR_BODY" ]; then + echo -e "$PR_BODY\n" >> release-notes.md + fi + fi + + # Create a draft release + gh release create v$VERSION \ + --draft \ + --title "v$VERSION" \ + --notes-file release-notes.md + fi + + # Commit the version change + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git add package.json + git commit -m "chore(release): bump version to $VERSION [skip ci]" + git push diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..a836c33 --- /dev/null +++ b/.releaserc @@ -0,0 +1,18 @@ +{ + "branches": ["main"], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + "@semantic-release/npm", + ["@semantic-release/github", { + "assets": [ + {"path": "dist/index.js", "label": "MCP Server Bundle"} + ] + }], + ["@semantic-release/git", { + "assets": ["package.json", "CHANGELOG.md"], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + }] + ] +}