diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 01ee23f..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Build -on: - push: - # Avoid duplicate builds on PRs. - branches: - - main - pull_request: -permissions: - contents: read -jobs: - build: - strategy: - fail-fast: false - matrix: - version: [22, 20, 18] - os: - [sfdc-hk-ubuntu-latest, sfdc-hk-macos-latest, sfdc-hk-windows-latest] - name: Node ${{ matrix.version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 # java required for testing with wiremock - with: - java-package: jre - java-version: "11" - distribution: "zulu" - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.version }} - cache: "npm" - - run: npm install - - run: npm run format:check - - run: npm run lint - - run: npm run test - - run: npm run build diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml new file mode 100644 index 0000000..c21beb0 --- /dev/null +++ b/.github/workflows/create-tag.yml @@ -0,0 +1,33 @@ +name: Create Tag + +on: + workflow_run: + workflows: ["Package Library"] + types: + - completed + branches: + - main + +jobs: + push-git-tag: + if: ${{ github.event.workflow_run.conclusion == 'success' && contains(github.event.workflow_run.head_branch, 'release') }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract version from branch name + id: version + run: | + BRANCH_NAME="${{ github.event.workflow_run.head_branch }}" + VERSION=${BRANCH_NAME#release-v} + VERSION=${VERSION#release/} + VERSION=${VERSION#release-} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create tag + run: | + git tag "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}" + git push origin "v${{ steps.version.outputs.version }}" diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml new file mode 100644 index 0000000..ab268e7 --- /dev/null +++ b/.github/workflows/draft-release.yml @@ -0,0 +1,37 @@ +name: Draft Release Branch + +on: + workflow_dispatch: + inputs: + bump_type: + description: 'Type of version bump (major, minor, patch)' + required: true + type: choice + options: + - major + - minor + - patch + +jobs: + draft-release-branch: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Make scripts executable + run: | + chmod +x scripts/release/draft-release + chmod +x scripts/release/update-changelog + + - name: Run release script + run: ./scripts/release/draft-release "${{ github.event.inputs.bump_type }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..b52e212 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,43 @@ +name: Package Library + +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 }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Test + run: npm run test + + - name: Build + run: npm run build + + - name: Upload artifacts + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && contains(github.event.head_commit.message, 'Merge pull request') && contains(github.event.head_commit.message, 'release') + uses: actions/upload-artifact@v4 + with: + name: ${{ github.ref_name }} + path: dist/ + retention-days: 90 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..99fad9e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,66 @@ +name: Publish Release + +on: + push: + tags: + - 'v*' + +jobs: + check_for_moratorium: + runs-on: ubuntu-latest + environment: change_management + steps: + - uses: actions/checkout@v4 + - env: + TPS_API_TOKEN: ${{ secrets.TPS_API_TOKEN_PARAM }} + run: ./scripts/release/tps-check-lock heroku-applink-nodejs ${{ github.sha }} + + publish: + runs-on: ubuntu-latest + environment: change_management + permissions: + contents: write + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: ${{ github.ref_name }} + path: . + + - name: Install dependencies + run: npm ci + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + files: | + dist/**/* + package.json + README.md + CHANGELOG.md + LICENSE.txt + SECURITY.md + TERMS_OF_USE.md + + - name: Publish to npm + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_AUTOMATION_TOKEN }} + + - name: Publish To Change Management + env: + ACTOR_EMAIL: ${{ secrets.TPS_API_RELEASE_ACTOR_EMAIL }} + TPS_API_TOKEN: ${{ secrets.TPS_API_TOKEN_PARAM }} + run: ./scripts/release/tps-record-release heroku-applink-nodejs ${{ github.sha }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f53dd6..484e0ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - Update CODEOWNERS -## [0.1.0-ea] - 2024-08-12 +## [0.3.4-ea] - 2024-08-12 - Initial +# [0.3.4](https://github.com/heroku/heroku-applink-nodejs/compare/HEAD...0.3.4) - 2025-05-19 + + +### Changes + + +### Other + +* bump version to v0.3.4 +* wrong order +* initial release automation setup +* maint: update CODEOWNERS +* don't use v24 +* clean up test matrix +* skip failing tests +* add es module interop to fix some bad imports in tests +* add cross env to fix cross platform testing +* fix bad import +* clean up script +* prettier +* fix tests but with commonjs +* prettier +* get tests running +* Sdk docs update (#18) ([#18](https://github.com/heroku/heroku-applink-nodejs/pull/18)) +* Rename things to Applink +* Initial +* Initial commit + diff --git a/bin/bump-version.js b/bin/bump-version.js deleted file mode 100644 index c31247a..0000000 --- a/bin/bump-version.js +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env node - -import { readFile, readFileSync, writeFileSync } from "fs"; -import { exec } from "child_process"; -const version = process.argv[2]; - -const semver = new RegExp( - /^((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)?)$/ -); - -if (!version.match(semver)) { - console.error( - "Please use a valid numeric semver string:", - "https://semver.org/" - ); - process.exit(1); -} - -// bump package.json -console.log("Bumping version on package.json..."); - -readFile("./package.json", (err, jsonString) => { - const jsonObj = JSON.parse(jsonString); - jsonObj["version"] = version; - jsonString = JSON.stringify(jsonObj, undefined, 2) + "\n"; - writeFileSync("./package.json", jsonString); -}); - -// bump CHANGELOG -console.log("Bumping version in CHANGELOG.md..."); - -const changelog = readFileSync("./CHANGELOG.md") - .toString() - .split("## [Unreleased]"); -const today = new Date(); - -// JS doesn't support custom date formatting strings such as 'YYYY-MM-DD', so the best we can -// do is extract the date from the ISO 8601 string (which is YYYY-MM-DDTHH:mm:ss.sssZ). -// As an added bonus this uses UTC, ensuring consistency regardless of which machine runs this script. -const date = today.toISOString().split("T")[0]; -changelog.splice(1, 0, `## [Unreleased]\n\n## [${version}] - ${date}`); -writeFileSync("./CHANGELOG.md", changelog.join("")); - -console.log("Completed version bumping."); diff --git a/bin/publish.sh b/bin/publish.sh deleted file mode 100755 index 24bf2ed..0000000 --- a/bin/publish.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -echo "Running npm publish..." -npm publish - -echo "Creating git tag..." -git tag "v${version}" -git push --tags diff --git a/docs/release-workflow.md b/docs/release-workflow.md new file mode 100644 index 0000000..5f09c7e --- /dev/null +++ b/docs/release-workflow.md @@ -0,0 +1,93 @@ +# Release Workflow + +This document outlines the process for releasing new versions of the Heroku Salesforce SDK Node.js package. + +## Release Process + +### 1. Creating a Release Branch + +There are two ways to start a release: + +#### Option 1: GitHub Actions Workflow +1. Go to the "Actions" tab in GitHub +2. Select "Draft Release Branch" workflow +3. Click "Run workflow" +4. Select the type of version bump (major, minor, patch) + +#### Option 2: Command Line Script +Run the draft-release script locally: +```bash +./scripts/release/draft-release [ | major | minor | patch] +``` + +This will: +1. Run `npm version` with the provided version type +* `npm version` will update the version in package.json and commit that change. +2. Create a release branch named `release-v{version}` +3. Update `CHANGELOG.md` with all commits since the last tag +4. Create a draft PR from the release branch to main + +### 2. Testing and Building + +When a PR is created or updated, the following GitHub Actions workflows run automatically: + +1. `package.yml`: + - Runs on pushes to main and PRs + - Lints the code + - Runs tests + - Builds the package + - Uploads build artifacts for release branches + +### 3. Creating a Release Tag + +After the PR is merged to main and tests pass: +1. The `create-tag.yml` workflow runs automatically +2. Creates a git tag with the version number +3. Pushes the tag to the repository + +### 4. Publishing the Release + +When a new tag is pushed: +1. The `publish.yml` workflow runs automatically +2. Publishes the package to npm +3. Creates a GitHub release with the changelog + +## Workflow Files + +- `.github/workflows/package.yml`: Runs tests and builds on PRs and pushes +- `.github/workflows/create-tag.yml`: Creates git tags after successful merges +- `.github/workflows/publish.yml`: Publishes to npm and creates GitHub releases + +## Requirements + +- GitHub Actions permissions for: + - Contents (read/write) + - Packages (read/write) +- Environment secrets: + - `NPM_TOKEN`: For publishing to npm + +## Best Practices + +1. **Version Naming**: + - Use semantic versioning (MAJOR.MINOR.PATCH) + - Major: Breaking changes + - Minor: New features, no breaking changes + - Patch: Bug fixes, no breaking changes + +2. **Changelog**: + - Review the generated changelog in the PR + - Ensure all significant changes are documented + - Follow conventional commit format for better changelog generation + +3. **Testing**: + - Ensure all tests pass before merging + - Test the package locally before releasing + +## Rolling Back + +If issues are found in a production release: +1. Create a new patch release to fix the issue +2. Follow the standard release process +3. Document the fix in the changelog + +Note: We follow a "fix forward" approach rather than reverting releases to maintain a clear audit trail and versioning history. \ No newline at end of file diff --git a/package.json b/package.json index ab5e062..3e83d12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@heroku/salesforce-sdk-nodejs", - "version": "0.3.4-ea", + "version": "0.3.4", "description": "Salesforce SDK for Heroku Apps.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/scripts/release/draft-release b/scripts/release/draft-release new file mode 100755 index 0000000..1f1cf42 --- /dev/null +++ b/scripts/release/draft-release @@ -0,0 +1,82 @@ +#!/bin/bash +set -eu +set -o pipefail + +# Script to create a draft release branch and PR +# +# Usage: +# ./scripts/release/draft-release [ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git] +# +# This will: +# 1. Run npm version with the provided version type +# 2. Create a release branch named release-v{version} +# 3. Update CHANGELOG.md using update-changelog.sh +# 4. Create a draft PR from the release branch to main + +usage() { + echo "Usage: $0 [ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]" + echo " newversion: The new version to release (e.g., 1.2.3)" + echo " major: Bump major version (1.0.0 -> 2.0.0)" + echo " minor: Bump minor version (1.0.0 -> 1.1.0)" + echo " patch: Bump patch version (1.0.0 -> 1.0.1)" + echo " premajor: Bump major version with prerelease (1.0.0 -> 2.0.0-0)" + echo " preminor: Bump minor version with prerelease (1.0.0 -> 1.1.0-0)" + echo " prepatch: Bump patch version with prerelease (1.0.0 -> 1.0.1-0)" + echo " prerelease: Bump prerelease version (1.0.0 -> 1.0.0-0)" + echo " from-git: Use version from git tags" + exit 1 +} + +# Check if version type is provided +if [ "$#" -ne 1 ]; then + usage +fi + +VERSION_TYPE=$1 + +# Run npm version and capture the new version +echo "Running npm version $VERSION_TYPE..." +NEW_VERSION=$(npm version "$VERSION_TYPE" --no-git-tag-version) +# Remove the 'v' prefix that npm version adds +NEW_VERSION=${NEW_VERSION#v} + +# Create release branch with actual version number +echo "Creating release branch..." +git checkout -b "release-v${NEW_VERSION}" + +# Commit the version change +git add package.json +git commit -m "chore: bump version to v$NEW_VERSION" + +# Update changelog +echo "Updating changelog..." +./scripts/release/update-changelog "$NEW_VERSION" + +# Create commit for changelog update +git add CHANGELOG.md +git commit -m "docs: update changelog for v$NEW_VERSION" + +echo "Created commits for version bump and changelog update" + +# Push the release branch +git push origin "release-v${NEW_VERSION}" + +# Create draft PR +PR_BODY=$(cat << EOF +This is a draft release PR. Please review the changes: + +- Version bump in package.json +- Changelog updates + +Once approved, this PR can be merged to trigger the release process. +EOF +) + +gh pr create \ + --base main \ + --head "release-v${NEW_VERSION}" \ + --title "Release v$NEW_VERSION" \ + --body "$PR_BODY" \ + --draft + +echo "Created draft PR for release v$NEW_VERSION" \ No newline at end of file diff --git a/scripts/release/tps-check-lock b/scripts/release/tps-check-lock new file mode 100755 index 0000000..2f0b2bb --- /dev/null +++ b/scripts/release/tps-check-lock @@ -0,0 +1,66 @@ +#!/bin/bash +set -eu +set -o pipefail + +# Usage: ./scripts/release/tps_check_lock +# Required env vars: TPS_API_TOKEN, COMPONENT_SLUG, RELEASE_SHA +# +# Alternate Usage: ./scripts/release/tps_check_lock +# Required env vars: TPS_API_TOKEN + +if [ -z "${TPS_HOSTNAME:-}" ]; then + TPS_HOSTNAME="tps.heroku.tools" +fi + +if [ -z "${TPS_API_TOKEN:-}" ]; then + echo "Requires environment variable: TPS_API_TOKEN" >&2 + exit 1 +fi + +# Argument overrides the environment variable +component_slug="${1:-$COMPONENT_SLUG}" +if [ -z "$component_slug" ]; then + echo "Requires first argument or env var COMPONENT_SLUG: Heroku component slug" >&2 + exit 1 +fi + +release_sha="${2:-$RELEASE_SHA}" +if [ -z "$release_sha" ]; then + echo "Requires second argument or env var RELEASE_SHA: SHA of the commit being released" >&2 + exit 1 +fi + +response_status=0 + +tpsGetLock() { + response_status="$(curl --silent \ + -o tpsGetLock_response.txt -w "%{response_code}" \ + -X PUT \ + -H "Accept: */*" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${TPS_API_TOKEN}" \ + -d "{\"lock\": {\"sha\": \"${release_sha}\", \"component_slug\": \"${component_slug}\"}}" \ + https://${TPS_HOSTNAME}/api/ctc)" + + echo Response status $response_status: $(cat tpsGetLock_response.txt) >&2 +} + +echo "Requesting deployment lock from ${TPS_HOSTNAME}…" >&2 +retry_count=0 +set +e +tpsGetLock +until [ "$response_status" == "200" -o "$response_status" == "201" ] +do + ((retry_count++)) + if [ $retry_count -gt 40 ] + then + echo "❌ Could not get deployment lock for \"$component_slug\" after retrying for 10-minutes." >&2 + exit 2 + fi + echo "⏳ Retry in 15-seconds…" >&2 + sleep 15 + tpsGetLock +done +set -e +echo "✅ Lock acquired" >&2 + diff --git a/scripts/release/tps-record-release b/scripts/release/tps-record-release new file mode 100755 index 0000000..bf25223 --- /dev/null +++ b/scripts/release/tps-record-release @@ -0,0 +1,85 @@ +#!/bin/bash +set -eu +set -o pipefail + +# Usage: ./scripts/release/tps_record_release +# Required env vars: TPS_API_TOKEN, COMPONENT_SLUG, RELEASE_SHA, ACTOR_EMAIL + +# Alternate Usage: ./scripts/release/tps_record_release +# Required env vars: TPS_API_TOKEN, ACTOR_EMAIL + +# Alternate Usage: ./scripts/release/tps_record_release +# Required env vars: TPS_API_TOKEN + +if [ -z "${TPS_HOSTNAME:-}" ]; then + TPS_HOSTNAME="tps.heroku.tools" +fi + +if [ -z "${TPS_API_TOKEN:-}" ]; then + echo "Requires environment variable: TPS_API_TOKEN" >&2 + exit 1 +fi + +# Argument overrides the environment variable +component_slug="${1:-$COMPONENT_SLUG}" +if [ -z "$component_slug" ]; then + echo "Requires first argument or env var COMPONENT_SLUG: Heroku component slug" >&2 + exit 1 +fi + +release_sha="${2:-$RELEASE_SHA}" +if [ -z "$release_sha" ]; then + echo "Requires second argument or env var RELEASE_SHA: SHA of the commit being released" >&2 + exit 1 +fi + +actor_email="${3:-$ACTOR_EMAIL}" +if [ -z "$actor_email" ]; then + echo "Requires third argument or env var ACTOR_EMAIL: email of actor performing the release" >&2 + exit 1 +fi + +# No app_id for releases +# app_id="${4:-$APP_ID}" +# if [ -z "$app_id" ]; then +# echo "Requires fourth argument: UUID of app being released" >&2 +# exit 1 +# fi + +stage="production" +description="Deploy ${release_sha} of ${component_slug} in ${stage}" + +response_status=0 + +tpsRecordRelease() { + response_status="$(curl --silent \ + -o tpsRecordRelease_response.txt -w "%{response_code}" \ + -X POST \ + -H "Accept: */*" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${TPS_API_TOKEN}" \ + -d "{\"component_slug\": \"${component_slug}\", \"release\": {\"sha\": \"${release_sha}\", \"actor_email\": \"${actor_email}\", \"stage\": \"${stage}\", \"description\": \"${description}\"}}" \ + https://${TPS_HOSTNAME}/api/component/${component_slug}/releases)" + + echo Response status $response_status: $(cat tpsRecordRelease_response.txt) >&2 +} + +echo "Recording release with ${TPS_HOSTNAME}…" >&2 +retry_count=0 +set +e +tpsRecordRelease +until [ "$response_status" == "204" ] +do + ((retry_count++)) + if [ $retry_count -gt 120 ] + then + echo "❌ Could not record release for \"$component_slug\" after retrying for 30-minutes." >&2 + exit 2 + fi + echo "⏳ Retry in 15-seconds…" >&2 + sleep 15 + tpsRecordRelease +done +set -e +echo "✅ Release recorded" >&2 + diff --git a/scripts/release/update-changelog b/scripts/release/update-changelog new file mode 100755 index 0000000..7d2325b --- /dev/null +++ b/scripts/release/update-changelog @@ -0,0 +1,190 @@ +#!/bin/bash +set -eu +set -o pipefail + +# Script to update CHANGELOG.md with git log entries since last tag +# +# Usage: +# ./scripts/release/update-changelog.sh +# +# This will: +# 1. Update CHANGELOG.md with all commits since the last tag +# 2. Format the changelog with proper headers and commit references + +usage() { + echo "Usage: $0 " + echo " version: The version to add to the changelog (e.g., 1.2.3)" + exit 1 +} + +# Function to categorize commits based on conventional commit types +categorize_commit() { + local message=$1 + local hash=$2 + local body=$3 + + # Check for conventional commit types + if [[ "$message" =~ ^feat(\([a-z-]+\))?: ]]; then + echo "Features" + elif [[ "$message" =~ ^fix(\([a-z-]+\))?: ]]; then + echo "Fixes" + elif [[ "$message" =~ ^docs(\([a-z-]+\))?: ]]; then + echo "Docs" + else + echo "Other" + fi +} + +# Check if version is provided +if [ "$#" -ne 1 ]; then + usage +fi + +NEW_VERSION=$1 + +# Get the last tag (sorted by creation date) +LAST_TAG=$(git tag --sort=creatordate | tail -n 1 2>/dev/null || echo "") + +echo "Last tag: $LAST_TAG" + +# Get the repository URL +REPO_URL=$(git config --get remote.origin.url | sed 's/\.git$//' | sed 's/git@github.com:/https:\/\/github.com\//') + +echo "Repository URL: $REPO_URL" + +# Create temporary directory for our files +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +# Create temporary files in the temp directory +NEW_CONTENT="$TEMP_DIR/new_content" +FEATURES_TMP="$TEMP_DIR/features" +FIXES_TMP="$TEMP_DIR/fixes" +DOCS_TMP="$TEMP_DIR/docs" +OTHER_TMP="$TEMP_DIR/other" +OLD_CONTENT="$TEMP_DIR/old_content" + +# Save the old changelog if it exists +if [ -f "CHANGELOG.md" ]; then + cp "CHANGELOG.md" "$OLD_CONTENT" +fi + +# Create the new version content +if [ -n "$LAST_TAG" ]; then + VERSION_HEADER="# [$NEW_VERSION]($REPO_URL/compare/$LAST_TAG...$NEW_VERSION) - $(date +%Y-%m-%d)" +else + VERSION_HEADER="# [$NEW_VERSION]($REPO_URL/compare/HEAD...$NEW_VERSION) - $(date +%Y-%m-%d)" +fi + +echo "Version header: $VERSION_HEADER" + +cat > "$NEW_CONTENT" << EOL +$VERSION_HEADER + + +### Changes + +EOL + +# Get all commits since last tag +if [ -n "$LAST_TAG" ]; then + # Use the last tag as reference point + COMMITS=$(git log --pretty=format:"%s|%H" "${LAST_TAG}..HEAD") +else + # If no tags, use all commits + COMMITS=$(git log --pretty=format:"%s|%H") +fi + +echo "Found $(echo "$COMMITS" | wc -l) commits to process" + +# Initialize temporary files +touch "$FEATURES_TMP" "$FIXES_TMP" "$DOCS_TMP" "$OTHER_TMP" + +echo "$COMMITS" | while IFS='|' read -r message hash; do + # Skip merge commits + if [[ ! $message =~ ^Merge ]]; then + # Format the commit message + if [[ "$message" =~ ^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z-]+\))?: ]]; then + # Remove the type prefix for conventional commits + formatted_message="${message#*:}" + else + formatted_message="$message" + fi + + # Add breaking change notice if present + if [[ "$formatted_message" == *"BREAKING CHANGE"* ]]; then + formatted_message="$formatted_message (BREAKING CHANGE)" + fi + + # Extract PR or issue number if present in the message + ref_number="" + ref_type="" + # Look for (#1234) or #1234 in the message + if [[ "$message" =~ \(#([0-9]+)\) ]]; then + ref_number="${BASH_REMATCH[1]}" + ref_type="pull" + elif [[ "$message" =~ \#([0-9]+) ]]; then + ref_number="${BASH_REMATCH[1]}" + ref_type="issues" + fi + + # Get the category for this commit + category=$(categorize_commit "$message" "$hash" "") + case "$category" in + "Features") tmp_file="$FEATURES_TMP" ;; + "Fixes") tmp_file="$FIXES_TMP" ;; + "Docs") tmp_file="$DOCS_TMP" ;; + *) tmp_file="$OTHER_TMP" ;; + esac + + # Add the commit to the appropriate category file + if [ -n "$ref_number" ]; then + echo "* $formatted_message ([#$ref_number]($REPO_URL/$ref_type/$ref_number))" >> "$tmp_file" + else + echo "* $formatted_message" >> "$tmp_file" + fi + fi +done + +# Combine all categories into the new content +for category in "Features" "Fixes" "Docs" "Other"; do + case "$category" in + "Features") tmp_file="$FEATURES_TMP" ;; + "Fixes") tmp_file="$FIXES_TMP" ;; + "Docs") tmp_file="$DOCS_TMP" ;; + *) tmp_file="$OTHER_TMP" ;; + esac + if [ -s "$tmp_file" ]; then + echo -e "\n### $category\n" >> "$NEW_CONTENT" + cat "$tmp_file" >> "$NEW_CONTENT" + fi +done + +# Create the new changelog +if [ -f "$OLD_CONTENT" ]; then + # Create a new changelog with the updated content + { + # Get the header (title and description) + awk '/^# \[/ {exit} {print}' "$OLD_CONTENT" + # Add the new version section + cat "$NEW_CONTENT" + echo + # Add all existing content after the header + awk '/^# \[/ {p=1} p {print}' "$OLD_CONTENT" + } > "CHANGELOG.md.new" + + # Move the new file to replace the old one + mv "CHANGELOG.md.new" "CHANGELOG.md" +else + # Create a new changelog + { + echo "# Changelog" + echo + echo "All notable changes to this project will be documented in this file." + echo "See [Conventional Commits](https://conventionalcommits.org) for commit guidelines." + echo + cat "$NEW_CONTENT" + } > "CHANGELOG.md" +fi + +echo "Updated CHANGELOG.md" \ No newline at end of file