diff --git a/.github/workflows/README.md b/.github/workflows/README.md index f389387..3c0537e 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -118,6 +118,56 @@ Changelogs are generated using git-cliff with the following approach: 3. **Format:** Follows [Keep a Changelog](https://keepachangelog.com/) format 4. **Grouping:** Commits are grouped by type (Added, Fixed, Changed, etc.) +### GitHub Integration Features + +When `GITHUB_TOKEN` is provided, git-cliff enhances changelogs with: + +- **PR Links**: Automatic links to pull requests in commit messages + - Format: `- commit message by @username in [#123](url)` + - Works with squash merges and regular merges + +- **Contributor Attribution**: Shows who made each change + - Format: `by @username` after each commit message + - Retrieved from GitHub API + +- **First-Time Contributors**: Special recognition section + - Shows new contributors separately: `### New Contributors` + - Format: `- @username made their first contribution in [#123](url)` + +- **Statistics**: Release metrics automatically calculated + - Commit count (total and conventional) + - Linked issues/PRs count + - Days since last release + +- **PR Labels**: Can be used for filtering + - Add `skip-release-notes` label to exclude commits from changelog + - Supports label-based grouping (advanced feature) + +**Example changelog entry with GitHub integration:** + +```markdown +## [0.2.0] - 2025-12-27 + +### Added +- feat: add search functionality by @username in [#42](https://github.com/JSONbored/safemocker/pull/42) +- feat: improve performance by @contributor in [#43](https://github.com/JSONbored/safemocker/pull/43) + +### New Contributors +- @newuser made their first contribution in [#42](https://github.com/JSONbored/safemocker/pull/42) + +### Statistics +- 5 commits in this release +- 4 conventional commits +- 2 linked issues/PRs +- 3 days since last release +``` + +**Configuration:** + +- `GITHUB_TOKEN` is automatically provided in workflows via `secrets.GITHUB_TOKEN` +- `[remote.github]` section in `cliff.toml` configures repository details +- GitHub API is used to fetch PR metadata, contributor info, and labels + ## Manual Operations ### Trigger Version Bump Manually @@ -165,18 +215,34 @@ Changelogs are generated using git-cliff with the following approach: **Solutions:** - Verify commits since last tag exist - Check git-cliff output in workflow logs -- Ensure `GITHUB_TOKEN` is set (for PR links) +- Ensure `GITHUB_TOKEN` is set (for PR links and GitHub integration) - Verify `cliff.toml` configuration is correct +- Check that commits follow conventional commit format + +### GitHub Integration Not Working + +**Issue:** PR links, usernames, or contributor info not appearing in changelog. + +**Solutions:** +- Verify `GITHUB_TOKEN` is set in workflow (automatically provided via `secrets.GITHUB_TOKEN`) +- Check that commits are associated with PRs (squash merges work fine) +- Ensure `[remote.github]` is configured in `cliff.toml` with correct owner/repo +- Verify GitHub API rate limits haven't been exceeded +- Check workflow logs for GitHub API errors +- Note: GitHub integration requires commits to be associated with PRs - direct commits to main won't have PR links ### npm Publish Fails **Issue:** npm publish fails with authentication error. **Solutions:** -- Verify trusted publishing is configured in npm +- Verify trusted publishing is configured in npm for repository and environment - Check `production` environment is set in workflow - Ensure `id-token: write` permission is set +- Verify `setup-node` is configured **before** `pnpm/action-setup` +- Check `registry-url: 'https://registry.npmjs.org'` is set - Verify package name and version in `package.json` +- See "npm Trusted Publishing" section above for detailed troubleshooting ### Tag Already Exists @@ -204,13 +270,103 @@ Contains: - npm scripts (optional, for local use) - Package metadata +## Squash Merge Support + +### How It Works + +When you **squash merge** a PR, GitHub creates a **single commit** on `main` with: +- **Commit message**: The PR title (should follow conventional commits: `feat:`, `fix:`, etc.) +- **Commit body**: The PR description (optional) + +**git-cliff automatically processes all commits**, including squash merge commits. The workflow: + +1. ✅ Squash merge creates a single commit on `main` +2. ✅ git-cliff processes this commit just like any other commit +3. ✅ Commit message is parsed using conventional commit rules +4. ✅ Changelog entry is generated based on commit type (feat → Added, fix → Fixed, etc.) + +### Best Practices for Squash Merges + +- **Use conventional commits in PR titles**: `feat: add feature`, `fix: resolve bug`, etc. +- **Include PR references**: The commit message can include `(#123)` which will be converted to a PR link +- **PR descriptions are optional**: git-cliff primarily uses the commit message (PR title) + +### Example + +**PR Title**: `feat: add search functionality (#42)` + +**After squash merge**: +- Creates commit: `feat: add search functionality (#42)` +- git-cliff processes it as a `feat:` commit +- Appears in changelog under "### Added" +- PR link is automatically added: `([#42](https://github.com/JSONbored/safemocker/pull/42))` + +### Configuration + +The `commit_preprocessors` in `cliff.toml` handle: +- Standard GitHub PR merge messages: `Merge pull request #123` +- PR references in commit messages: `(#123)` +- Squash merge commits (processed automatically) + +## npm Trusted Publishing + +### Overview + +The release workflow uses **npm Trusted Publishing** with OIDC (OpenID Connect) for secure, tokenless authentication. This eliminates the need for `NODE_AUTH_TOKEN` secrets. + +### Requirements + +1. **Environment name**: Must match npm trusted publisher configuration (currently `production`) +2. **Permissions**: `id-token: write` is required (already set in workflow) +3. **setup-node order**: Must be configured **before** pnpm setup for OIDC to work +4. **registry-url**: Must be set to `https://registry.npmjs.org` +5. **--provenance flag**: Creates signed provenance statements + +### How It Works + +1. GitHub Actions generates an OIDC token automatically +2. `setup-node@v4` with `registry-url` configures npm to use OIDC +3. `npm publish --provenance` uses the OIDC token for authentication +4. npm verifies the token against your trusted publisher configuration +5. Package is published with signed provenance + +### Verification + +After publishing, the workflow verifies the package is available on npm: + +```bash +npm view "@jsonbored/safemocker@$VERSION" version +``` + +If this fails, the workflow will report an error. + +### Troubleshooting npm Trusted Publishing + +**Issue**: npm publish fails with authentication error + +**Solutions**: +1. Verify environment name matches npm trusted publisher config (`production`) +2. Check that `id-token: write` permission is set in workflow +3. Ensure `setup-node` is configured before `pnpm/action-setup` +4. Verify `registry-url: 'https://registry.npmjs.org'` is set +5. Check npm trusted publisher configuration: https://docs.npmjs.com/trusted-publishers/ + +**Issue**: Package not found after publish + +**Solutions**: +1. Wait a few seconds - npm registry may need time to update +2. Check npm registry directly: https://www.npmjs.com/package/@jsonbored/safemocker +3. Verify the version number matches exactly +4. Check workflow logs for publish errors + ## Best Practices 1. **Use Conventional Commits:** Always use conventional commit format (`feat:`, `fix:`, etc.) -2. **Squash Merges:** Use squash merges for PRs to maintain clean history +2. **Squash Merges:** Use squash merges for PRs to maintain clean history (fully supported) 3. **Don't Commit Changelog Manually:** Let workflows handle changelog updates 4. **Test Locally:** Use `pnpm exec git-cliff --bumped-version` to preview version 5. **Review Before Merging:** Check PR commits to ensure correct version bump +6. **Verify npm Publishing:** Check workflow logs to ensure package was published successfully ## Local Development diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc4dd32..97b7a22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,15 +21,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' cache: 'pnpm' @@ -47,3 +47,27 @@ jobs: run: pnpm test:coverage continue-on-error: true + - name: Validate changelog format + run: | + if [ -f CHANGELOG.md ]; then + # Verify changelog has proper structure + if ! grep -q "^# Changelog" CHANGELOG.md; then + echo "❌ Error: Invalid changelog header - expected '# Changelog'" + exit 1 + fi + + # Verify changelog follows Keep a Changelog format + if ! grep -q "Keep a Changelog" CHANGELOG.md; then + echo "⚠️ Warning: Changelog may not follow Keep a Changelog format" + fi + + # Verify changelog has at least one version entry or [Unreleased] section + if ! grep -qE "^## \[" CHANGELOG.md; then + echo "⚠️ Warning: Changelog has no version entries" + fi + + echo "✅ Changelog format validation passed" + else + echo "ℹ️ CHANGELOG.md not found (this is okay for new repositories)" + fi + diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 22f3ec2..7eb0283 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -13,6 +13,19 @@ # **Triggers:** # - Tag push matching v*.*.* pattern (automatic, triggered by version-bump.yml) # - Manual workflow_dispatch (with tag input for re-publishing) +# +# **npm Trusted Publishing Requirements:** +# CRITICAL: This workflow filename (publish-release.yml) MUST exactly match the +# workflow filename configured in npm trusted publisher settings. +# +# Required npm trusted publisher configuration: +# - Organization/User: JSONbored +# - Repository: safemocker (or JSONbored/safemocker) +# - Workflow filename: publish-release.yml (must match this file name exactly) +# - Environment: production (must match the environment name below) +# +# See: https://docs.npmjs.com/trusted-publishers/ +# Documentation: .cursor/npm-trusted-publishing-setup.md name: Publish Release on: @@ -32,7 +45,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 - # CRITICAL: Must match npm trusted publisher environment configuration + # CRITICAL: Environment name must exactly match npm trusted publisher configuration + # This workflow filename (publish-release.yml) must also match npm config + # Required npm settings: + # - Workflow filename: publish-release.yml + # - Environment: production + # - Repository: JSONbored/safemocker + # - Organization: JSONbored environment: production permissions: @@ -41,7 +60,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: # Full history needed for changelog extraction fetch-depth: 0 @@ -79,20 +98,28 @@ jobs: echo "TAG=$TAG" >> $GITHUB_OUTPUT echo "✅ Extracted version: $VERSION from tag: $TAG" - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Setup Node.js (with OIDC for trusted publishing) + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' registry-url: 'https://registry.npmjs.org' - cache: 'pnpm' - # OIDC is automatically used when id-token: write permission is set + # CRITICAL: setup-node must be configured BEFORE pnpm setup for OIDC to work correctly + # OIDC token is automatically used when id-token: write permission is set # No NODE_AUTH_TOKEN needed for trusted publishing + # Note: cache is configured after pnpm is installed - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 with: version: 10 + - name: Configure Node.js cache + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '20' + cache: 'pnpm' + # Configure caching after pnpm is available + - name: Install dependencies run: pnpm install --frozen-lockfile @@ -128,14 +155,53 @@ jobs: echo "🔍 DEBUG: Publishing @jsonbored/safemocker@$VERSION to npm..." echo "🔍 DEBUG: Using trusted publishing (OIDC) - no NODE_AUTH_TOKEN needed" + echo "🔍 DEBUG: Environment: ${{ github.environment }}" + echo "🔍 DEBUG: Repository: ${{ github.repository }}" # For trusted publishing, npm automatically uses OIDC token from GitHub Actions # The --provenance flag creates a signed provenance statement # Ensure trusted publishing is configured in npm: https://docs.npmjs.com/trusted-publishers/ - npm publish --access public --provenance + # Trusted publishing requires: + # 1. Environment name matches npm trusted publisher config (currently: production) + # 2. id-token: write permission (already set in permissions) + # 3. registry-url set in setup-node (already configured) + + if ! npm publish --access public --provenance; then + echo "❌ Error: npm publish failed" >&2 + echo "🔍 DEBUG: Check that trusted publishing is configured in npm for:" >&2 + echo " - Organization/User: JSONbored" >&2 + echo " - Repository: ${{ github.repository }}" >&2 + echo " - Workflow filename: publish-release.yml (must match exactly)" >&2 + echo " - Environment: production (must match exactly)" >&2 + echo " - See: https://docs.npmjs.com/trusted-publishers/" >&2 + echo " - Documentation: .cursor/npm-trusted-publishing-setup.md" >&2 + exit 1 + fi echo "✅ Published @jsonbored/safemocker@$VERSION to npm" + - name: Verify npm publish + run: | + set -e # Exit on error + + VERSION="${{ steps.version.outputs.VERSION }}" + + echo "🔍 DEBUG: Verifying package is published on npm..." + + # Wait a moment for npm registry to update + sleep 2 + + # Verify package exists on npm + if ! npm view "@jsonbored/safemocker@$VERSION" version > /dev/null 2>&1; then + echo "❌ Error: Package not found on npm after publish" >&2 + echo "🔍 DEBUG: Attempted to verify: @jsonbored/safemocker@$VERSION" >&2 + echo "🔍 DEBUG: This may indicate the publish failed or npm registry hasn't updated yet" >&2 + exit 1 + fi + + PUBLISHED_VERSION=$(npm view "@jsonbored/safemocker@$VERSION" version) + echo "✅ Verified: @jsonbored/safemocker@$PUBLISHED_VERSION is published on npm" + - name: Extract changelog for version id: changelog run: | @@ -196,7 +262,7 @@ jobs: head -10 /tmp/release-notes.md || echo "(empty)" - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 with: tag_name: ${{ steps.version.outputs.TAG }} name: ${{ steps.version.outputs.TAG }} diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 8b5b76c..8c8f968 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -46,21 +46,23 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: # CRITICAL: fetch-depth: 0 needed for git-cliff to access full commit history fetch-depth: 0 - # Use main branch for workflow_dispatch, otherwise use PR ref - ref: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.ref }} + # Always checkout main branch (both PR merges and workflow_dispatch should use main) + # For PR merges: github.ref points to PR ref, but we want the merged state on main + # For workflow_dispatch: use main branch + ref: main token: ${{ secrets.GITHUB_TOKEN }} - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' cache: 'pnpm' @@ -275,11 +277,26 @@ jobs: echo "🔍 DEBUG: Generating changelog for version $NEW_VERSION" echo "🔍 DEBUG: Base tag: $LAST_TAG" + # Verify GITHUB_TOKEN is set for GitHub API access + if [ -z "$GITHUB_TOKEN" ]; then + echo "⚠️ Warning: GITHUB_TOKEN is not set. GitHub integration features (PR links, usernames, contributors) will not be available." + echo "🔍 DEBUG: This is expected if running locally without GITHUB_TOKEN" + else + echo "✅ GITHUB_TOKEN is set - GitHub API integration enabled" + echo "🔍 DEBUG: GitHub API will be used for:" + echo " - PR links in commit messages" + echo " - Contributor usernames" + echo " - PR titles and labels" + echo " - First-time contributor detection" + fi + # Check if changelog already has this version (from previous failed run) - if grep -q "## \[$NEW_VERSION\]" CHANGELOG.md 2>/dev/null; then + # Use -F for fixed string matching (brackets are literal, not regex) + if grep -Fq "## [$NEW_VERSION]" CHANGELOG.md 2>/dev/null; then echo "⚠️ Changelog already contains version $NEW_VERSION, checking if it has content..." # If the version exists but has no content (only header), regenerate - if grep -A 10 "## \[$NEW_VERSION\]" CHANGELOG.md | grep -q "^###"; then + # Use -F for fixed string matching + if grep -FA 10 "## [$NEW_VERSION]" CHANGELOG.md | grep -q "^###"; then echo "✅ Changelog already has content for version $NEW_VERSION, skipping generation" echo "CHANGELOG_UPDATED=true" >> $GITHUB_OUTPUT exit 0 @@ -309,14 +326,28 @@ jobs: # Use --tag with --latest --unreleased to generate versioned changelog directly # This generates a changelog entry with [NEW_VERSION] instead of [Unreleased] - # --prepend is configured in cliff.toml, so it will prepend to CHANGELOG.md + # --output CHANGELOG.md explicitly writes to the file (required when using --tag) + # --prepend is configured in cliff.toml, so it will prepend to existing CHANGELOG.md # --verbose provides debugging information # --topo-order ensures tags are sorted topologically (important for complex branching) echo "🔍 DEBUG: Running git-cliff to generate versioned changelog..." - echo "🔍 Command: pnpm exec git-cliff --config cliff.toml --tag v$NEW_VERSION --latest --unreleased --verbose --topo-order" + echo "🔍 Command: pnpm exec git-cliff --config cliff.toml --tag v$NEW_VERSION --latest --unreleased --output CHANGELOG.md --verbose --topo-order" # Generate versioned changelog (this prepends to CHANGELOG.md with [NEW_VERSION] header) - pnpm exec git-cliff --config cliff.toml --tag "v$NEW_VERSION" --latest --unreleased --verbose --topo-order + # CRITICAL: --output is required to write to file when using --tag + # GITHUB_TOKEN enables GitHub API access for PR links, usernames, and contributor data + echo "🔍 DEBUG: Executing git-cliff with GitHub integration..." + pnpm exec git-cliff --config cliff.toml --tag "v$NEW_VERSION" --latest --unreleased --output CHANGELOG.md --verbose --topo-order + + # Verify GitHub integration worked (check for PR links or usernames in changelog) + if [ -n "$GITHUB_TOKEN" ]; then + if grep -qE "\[#[0-9]+\]|@[a-zA-Z0-9-]+" CHANGELOG.md 2>/dev/null; then + echo "✅ GitHub integration successful - PR links and/or usernames found in changelog" + else + echo "⚠️ Warning: GitHub integration may not have worked - no PR links or usernames found" + echo "🔍 DEBUG: This may be normal if commits don't have associated PRs" + fi + fi # Verify git-cliff generated content if [ ! -f CHANGELOG.md ]; then @@ -325,23 +356,26 @@ jobs: fi # Verify the version entry was created - if ! grep -q "## \[$NEW_VERSION\]" CHANGELOG.md; then - echo "❌ Changelog was generated but version $NEW_VERSION entry not found" - echo "🔍 DEBUG: First 20 lines of CHANGELOG.md:" - head -20 CHANGELOG.md + # Use -F for fixed string matching (brackets are literal, not regex) + if ! grep -Fq "## [$NEW_VERSION]" CHANGELOG.md; then + echo "❌ Error: Changelog was generated but version $NEW_VERSION entry not found" >&2 + echo "🔍 DEBUG: First 20 lines of CHANGELOG.md:" >&2 + head -20 CHANGELOG.md >&2 exit 1 fi + # Validate changelog has content (not just header) + if ! grep -FA 5 "## [$NEW_VERSION]" CHANGELOG.md | grep -q "^###"; then + echo "⚠️ Warning: Changelog section for version $NEW_VERSION appears empty (no subsections found)" + echo "🔍 DEBUG: This may indicate no commits were categorized for this version" + else + echo "✅ Changelog generated with content for version $NEW_VERSION" + fi + # Check if changelog has content (more than just the header) VERSION_LINES=$(awk -v version="$NEW_VERSION" '/^## \[/ { if (index($0, "[" version "]") > 0) { found=1; next } if (found && /^## \[/) exit; if (found) count++ } END { print count+0 }' CHANGELOG.md) echo "🔍 DEBUG: Version $NEW_VERSION section has $VERSION_LINES lines of content" - if [ "$VERSION_LINES" -gt 2 ]; then - echo "✅ Changelog generated with content for version $NEW_VERSION" - else - echo "⚠️ Changelog generated but version $NEW_VERSION section appears empty" - fi - # Verify changelog was updated if git diff --quiet CHANGELOG.md; then echo "⚠️ Changelog was not updated" @@ -407,22 +441,41 @@ jobs: echo "🔍 DEBUG: Pushing commit to main..." # Push commit first (ensures commit is on remote before tag) - git push --force-with-lease origin main + # Use regular push (not force) since we're pushing to main after merge + if ! git push origin main; then + echo "❌ Error: Git push to main failed" >&2 + echo "🔍 DEBUG: This may indicate a conflict or permission issue" >&2 + exit 1 + fi echo "🔍 DEBUG: Verifying commit push succeeded..." # Verify commit push succeeded - git fetch origin main --depth=1 + git fetch origin main LOCAL_HEAD=$(git rev-parse HEAD) REMOTE_HEAD=$(git rev-parse origin/main) if [ "$LOCAL_HEAD" != "$REMOTE_HEAD" ]; then - echo "❌ Git push failed: local HEAD ($LOCAL_HEAD) does not match origin/main ($REMOTE_HEAD)" >&2 + echo "❌ Error: Git push failed - local HEAD ($LOCAL_HEAD) does not match origin/main ($REMOTE_HEAD)" >&2 + echo "🔍 DEBUG: This indicates the push did not succeed" >&2 exit 1 fi echo "✅ Commit push verified: local and remote are in sync" echo "🔍 DEBUG: Creating tag v$NEW_VERSION..." + # Extract changelog summary for tag message (first 3 commit messages or first 200 chars) + CHANGELOG_SUMMARY=$(grep -A 50 "## \[$NEW_VERSION\]" CHANGELOG.md 2>/dev/null | grep "^- " | head -3 | sed 's/^- //' | tr '\n' '; ' | cut -c1-200 || echo "") + + # Build tag message with version and summary + # Use a temporary file to handle multi-line tag messages safely + TAG_MSG_FILE=$(mktemp) + echo "Release v$NEW_VERSION" > "$TAG_MSG_FILE" + if [ -n "$CHANGELOG_SUMMARY" ]; then + echo "" >> "$TAG_MSG_FILE" + echo "$CHANGELOG_SUMMARY" >> "$TAG_MSG_FILE" + fi + # Create and push tag separately (ensures tag push event triggers publish-release.yml workflow) - git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" + git tag -a "v$NEW_VERSION" -F "$TAG_MSG_FILE" + rm -f "$TAG_MSG_FILE" echo "🔍 DEBUG: Pushing tag v$NEW_VERSION..." git push origin "v$NEW_VERSION" diff --git a/cliff.toml b/cliff.toml index a235275..05f7d69 100644 --- a/cliff.toml +++ b/cliff.toml @@ -18,18 +18,47 @@ body = """ {% for group, commits in commits | group_by(attribute="group") %}\ ### {{ group | upper_first }} -{% for commit in commits %} -- {{ commit.message | split(pat="\n") | first | trim }} +{% for commit in commits %}\ +{% if commit.remote.pr_labels is containing("skip-release-notes") %}\ +{% else %}\ +- {{ commit.message | split(pat="\n") | first | trim }}\ +{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}\ +{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}](https://github.com/JSONbored/safemocker/pull/{{ commit.remote.pr_number }}){%- endif %} +{% endif %}\ {% endfor %} {% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length > 0 %} +### New Contributors +{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} +- @{{ contributor.username }} made their first contribution in [#{{ contributor.pr_number }}](https://github.com/JSONbored/safemocker/pull/{{ contributor.pr_number }}) +{% endfor %} + +{% endif %}\ +{% if statistics %} +### Statistics +- {{ statistics.commit_count }} commit{% if statistics.commit_count != 1 %}s{% endif %} in this release +- {{ statistics.conventional_commit_count }} conventional commit{% if statistics.conventional_commit_count != 1 %}s{% endif %} +{% if statistics.links | length > 0 %} +- {{ statistics.links | length }} linked issue{% if statistics.links | length != 1 %}s{% endif %}/PR{% if statistics.links | length != 1 %}s{% endif %} +{% endif %} +{% if statistics.days_passed_since_last_release %} +- {{ statistics.days_passed_since_last_release }} day{% if statistics.days_passed_since_last_release != 1 %}s{% endif %} since last release +{% endif %} + +{% endif %}\ {% else %}\ ## [Unreleased] {% for group, commits in commits | group_by(attribute="group") %}\ ### {{ group | upper_first }} -{% for commit in commits %} -- {{ commit.message | split(pat="\n") | first | trim }} +{% for commit in commits %}\ +{% if commit.remote.pr_labels is containing("skip-release-notes") %}\ +{% else %}\ +- {{ commit.message | split(pat="\n") | first | trim }}\ +{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}\ +{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}](https://github.com/JSONbored/safemocker/pull/{{ commit.remote.pr_number }}){%- endif %} +{% endif %}\ {% endfor %} {% endfor %}\ @@ -60,6 +89,14 @@ filter_commits = true # Ignores other tags like alpha, beta, rc, etc. tag_pattern = "^v[0-9]+\\.[0-9]+\\.[0-9]+$" +# Squash Merge Support: +# git-cliff automatically processes all commits in the repository, including squash merge commits. +# When a PR is squash merged, GitHub creates a single commit with: +# - Commit message: The PR title (should follow conventional commits: feat:, fix:, etc.) +# - Commit body: The PR description (optional) +# git-cliff processes this commit just like any other commit, so squash merges work seamlessly. +# The commit_preprocessors above handle PR references and merge messages automatically. + commit_parsers = [ { message = "^feat", group = "Added" }, { message = "^fix", group = "Fixed" }, @@ -74,6 +111,12 @@ commit_parsers = [ { message = "^ci", skip = true }, { message = "^build", skip = true }, { message = "^revert", group = "Fixed" }, + # Advanced: PR label-based grouping (optional) + # Uncomment to group commits by PR labels instead of commit message prefixes + # This requires GitHub integration (GITHUB_TOKEN) to work + # Example: Group commits with "enhancement" label into "Enhancements" group + # { field = "github.pr_labels", pattern = "enhancement", group = "Enhancements" }, + # { field = "github.pr_labels", pattern = "bug", group = "Bug Fixes" }, ] protect_breaking_commits = true @@ -87,15 +130,28 @@ link_parsers = [ commit_preprocessors = [ # Handle GitHub PR merge messages (standard format) + # Note: Squash merges create a single commit with the PR title/description + # git-cliff processes all commits, including squash merge commits + # The commit message from squash merges should follow conventional commits (feat:, fix:, etc.) { pattern = '^Merge pull request #(\\d+)', replace = "([#${1}](https://github.com/JSONbored/safemocker/pull/${1}))" }, + # Handle merge commits with PR numbers in different formats + { pattern = '^Merge PR #(\\d+)', replace = "([#${1}](https://github.com/JSONbored/safemocker/pull/${1}))" }, # Handle Azure DevOps PR merge messages { pattern = '^Merged PR #\\d+: (.+)', replace = "$1" }, - # Handle PR references in commit messages (e.g., "fix: issue (#123)") + # Handle PR references in commit messages (e.g., "fix: issue (#123)" or "fix: issue #123") + # This works with squash merges where PR references are included in the commit message { pattern = '\\(#(\\d+)\\)', replace = "([#${1}](https://github.com/JSONbored/safemocker/pull/${1}))" }, + { pattern = ' #(\\d+)(?![0-9])', replace = " [#${1}](https://github.com/JSONbored/safemocker/pull/${1})" }, + # Handle co-authored commits (Co-authored-by: user@example.com) + # Extract co-author information for better attribution + { pattern = 'Co-authored-by: (.+)', replace = "Co-authored-by: $1" }, # Normalize multiple spaces to single space { pattern = ' +', replace = " " }, # Remove trailing periods from commit messages for consistency { pattern = '\\.$', replace = "" }, + # Clean up common merge commit prefixes + { pattern = '^Merge branch \'[^\']+\' into ', replace = "" }, + { pattern = '^Merge .+ into ', replace = "" }, ] [bump]