diff --git a/.github/workflows/build.yml b/.github/workflows/package.yml similarity index 65% rename from .github/workflows/build.yml rename to .github/workflows/package.yml index 01ee23f..2daabd6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/package.yml @@ -1,9 +1,8 @@ -name: Build +name: Package Library + on: push: - # Avoid duplicate builds on PRs. - branches: - - main + branches: [main] pull_request: permissions: contents: read @@ -33,3 +32,10 @@ jobs: - run: npm run lint - run: npm run test - run: npm run build + - name: Upload artifacts for release + 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: heroku-applink-nodejs + path: dist/ + retention-days: 90 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..0328fa4 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,83 @@ +name: Publish to NPM and Change Management + +on: + workflow_run: + workflows: ["Create Github Tag and Release"] + types: + - completed + branches: + - main + +permissions: + contents: write + actions: read + id-token: write + +jobs: + check_for_moratorium: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + 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: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + needs: check_for_moratorium + 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: Get latest build workflow run + id: get_workflow + env: + GH_TOKEN: ${{ github.token }} + run: | + # Get the most recent successful build workflow run for this commit + RUN_ID=$(gh run list --workflow "test-lint-build.yml" --branch main --json databaseId,conclusion,headSha --jq ".[] | select(.headSha == \"${{ github.sha }}\" and .conclusion == \"success\") | .databaseId" | head -n 1) + + if [ -z "$RUN_ID" ]; then + echo "No successful build found for commit ${{ github.sha }}" + exit 1 + fi + + echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT + echo "Found build workflow run: $RUN_ID" + + - name: Download build artifacts + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p dist + gh run download ${{ steps.get_workflow.outputs.run_id }} --name heroku-applink-nodejs --dir dist/ + + - name: Install dependencies + run: npm ci + + - 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 }} + # Failure to record the release should not fail the workflow for now. + continue-on-error: true + run: ./scripts/release/tps-record-release heroku-applink-nodejs ${{ github.sha }} diff --git a/.github/workflows/tag-and-release.yml b/.github/workflows/tag-and-release.yml new file mode 100644 index 0000000..7a6511a --- /dev/null +++ b/.github/workflows/tag-and-release.yml @@ -0,0 +1,74 @@ +name: Create Github Tag and Release + +on: + workflow_run: + workflows: ["Package Library"] + types: + - completed + branches: + - main + +permissions: + contents: write + actions: read + id-token: write + +jobs: + push-git-tag: + if: ${{ github.event.workflow_run.conclusion == 'success' && contains(github.event.workflow_run.head_commit.message, 'Merge pull request') && contains(github.event.workflow_run.head_commit.message, 'release-v') }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download artifact using GitHub CLI + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p dist + gh run download ${{ github.event.workflow_run.id }} --name release-artifacts-main --dir dist/ + + - name: Extract version from branch name + id: version + run: | + VERSION=$(echo "${{ github.event.workflow_run.head_commit.message }}" | grep -o 'release-v[0-9]\+\.[0-9]\+\.[0-9]\+' | sed 's/release-v//') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - name: Configure Git + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "github-actions@github.com" + + - name: Create and push Github tag + run: | + echo "Creating tag v${{ steps.version.outputs.version }}" + git tag -s "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}" + echo "Pushing tag to origin..." + git push origin "v${{ steps.version.outputs.version }}" + echo "Verifying tag was pushed:" + git ls-remote --tags origin "v${{ steps.version.outputs.version }}" + + - name: Get tag name + id: get_tag + run: | + TAG=$(git describe --tags --abbrev=0) + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "Using tag: $TAG" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.get_tag.outputs.tag }} + generate_release_notes: true + files: | + dist/**/* + package.json + README.md + CHANGELOG.md + LICENSE.txt + SECURITY.md + TERMS_OF_USE.md diff --git a/.gitignore b/.gitignore index 3fc2826..dad4ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,10 @@ tsconfig.tsbuildinfo .DS_Store src/**/*.js src/**/*.js.map + +# Salesforce +.codegenie/ + +# Heroku TPS TEMP +tpsGetLock_response.txt +tpsRecordRelease_response.txt \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f53dd6..06bb483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Changelog + All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.3.4-ea] - 2024-03-21 -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased -- Update CODEOWNERS +### Changes -## [0.1.0-ea] - 2024-08-12 +### Features +* Initial release ([c5d593f](https://github.com/heroku/heroku-applink-nodejs/commit/c5d593fa3c0f37607239e3ded7c2c24d7354383c)) -- Initial 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..237e712 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,6 @@ "format:check": "prettier --check .", "format:write": "prettier --write .", "prepack": "tsc -b --clean && tsc -b --force", - "bump": "node bin/bump-version.js", - "release": "bin/publish.sh", "docs": "typedoc" }, "files": [ diff --git a/scripts/release/draft-release b/scripts/release/draft-release new file mode 100755 index 0000000..6b110ab --- /dev/null +++ b/scripts/release/draft-release @@ -0,0 +1,102 @@ +#!/bin/bash +set -eu +set -o pipefail + +# draft-release +# Script to create a release branch and PR for a new version +# +# Usage: +# ./release/draft-release +# bump_type: major, minor, or patch +# +# Example: +# ./scripts/release/draft-release minor +# ./scripts/release/draft-release patch + + +# Function to display usage +usage() { + echo "Usage: $0 " + echo " bump_type: major, minor, or patch" + exit 1 +} + +# Function to handle errors +handle_error() { + local line_no=$1 + local error_code=$2 + local error_command=$3 + echo "Error occurred in line $line_no (exit code $error_code): $error_command" + exit 1 +} + +# Set up error handling +trap 'handle_error ${LINENO} $? "$BASH_COMMAND"' ERR + +# Check if required arguments are provided +if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + usage +fi + +BUMP_TYPE=$1 + +# Validate bump type +if [[ ! "$BUMP_TYPE" =~ ^(major|minor|patch)$ ]]; then + echo "Error: bump_type must be major, minor, or patch" + usage +fi + +# 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} + +# Validate semver format +if ! [[ $NEW_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$ ]]; then + echo "Error: Invalid semver format: $NEW_VERSION" + echo "Please use a valid numeric semver string: https://semver.org/" + exit 1 +fi + +# 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..82553af --- /dev/null +++ b/scripts/release/tps-record-release @@ -0,0 +1,84 @@ +#!/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..4ca4f97 --- /dev/null +++ b/scripts/release/update-changelog @@ -0,0 +1,200 @@ +#!/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 +} + +# Check if version is provided +if [ "$#" -ne 1 ]; then + usage +fi + +NEW_VERSION=$1 + +# Function to categorize commit message +categorize_commit() { + local message=$1 + local hash=$2 + local body=$3 + + # First try conventional commit format + if [[ "$message" =~ ^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z-]+\))?: ]]; then + local type="${BASH_REMATCH[1]}" + case "$type" in + feat) echo "Features" ;; + fix) echo "Fixes" ;; + docs) echo "Docs" ;; + *) echo "Other" ;; + esac + return + fi + + # If not conventional, analyze message content + message_lower=$(echo "$message" | tr '[:upper:]' '[:lower:]') + + # Features + if [[ "$message_lower" =~ ^(add|create|implement|new|update|upgrade|enhance|improve|support|adds|creates|implements|updates|upgrades|enhances|improves|supports) ]]; then + echo "Features" + return + fi + + # Bug Fixes + if [[ "$message_lower" =~ ^(fix|fixes|fixed|resolve|resolves|resolved|correct|corrects|corrected|bug|issue|error|err|typo) ]]; then + echo "Fixes" + return + fi + + # Documentation + if [[ "$message_lower" =~ ^(doc|docs|document|documentation|readme|comment|comments|note|notes|changelog) ]]; then + echo "Docs" + return + fi + + # Default to other + echo "Other" +} + +# 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 + + # 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 with commit hash + echo "* $formatted_message ([${hash:0:7}]($REPO_URL/commit/$hash))" >> "$tmp_file" + 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" + # Add a blank line before the next version + 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