diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..50c1009ee --- /dev/null +++ b/.dockerignore @@ -0,0 +1,69 @@ +# Development files +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ +.nyc_output +*.test.js + +# Build artifacts +dist/ +build/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Documentation +README.md +CHANGELOG.md +docs/ + +# Scripts not needed in production +scripts/take-screenshots.js +playwright.config.js + +# Temporary files +temp-release/ +*.tmp +*.log + +# Test files +test/ +tests/ +__tests__/ +*.test.js +*.spec.js + +# Data directories +data/ +config/ + +# Development configs +.eslintrc* +.prettierrc* +tailwind.config.js + +# Package lock files - KEEP package-lock.json for npm ci +# package-lock.json \ No newline at end of file diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml new file mode 100644 index 000000000..9eb98ba1d --- /dev/null +++ b/.github/workflows/rc-release.yml @@ -0,0 +1,142 @@ +name: Release Candidate + +on: + push: + branches: + - develop + +jobs: + create-rc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version from package.json + id: version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Check if RC release needed + id: check + run: | + # Get base version (remove any existing RC suffix) + BASE_VERSION=$(echo "${{ steps.version.outputs.version }}" | sed 's/-rc[0-9]*$//') + + # Get the latest RC tag for this base version + LATEST_RC=$(git tag -l "v${BASE_VERSION}-rc*" | sort -V | tail -n1) + + if [ -z "$LATEST_RC" ]; then + echo "rc_number=1" >> $GITHUB_OUTPUT + echo "create_release=true" >> $GITHUB_OUTPUT + echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT + else + # Extract RC number and increment + RC_NUM=$(echo $LATEST_RC | grep -o 'rc[0-9]*' | grep -o '[0-9]*') + NEXT_RC=$((RC_NUM + 1)) + + # Check if there are new commits since last RC + COMMITS_SINCE=$(git rev-list --count $LATEST_RC..HEAD) + if [ "$COMMITS_SINCE" -gt 0 ]; then + echo "rc_number=$NEXT_RC" >> $GITHUB_OUTPUT + echo "create_release=true" >> $GITHUB_OUTPUT + echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT + else + echo "create_release=false" >> $GITHUB_OUTPUT + fi + fi + + - name: Update package.json with RC version + if: steps.check.outputs.create_release == 'true' + run: | + NEW_VERSION="${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }}" + echo "Updating package.json version to: $NEW_VERSION" + + # Update package.json version + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + pkg.version = '$NEW_VERSION'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + " + + # Configure git + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Commit the version change + git add package.json + git commit -m "chore: bump version to $NEW_VERSION for RC release" + git push origin develop + + - name: Set up Docker Buildx + if: steps.check.outputs.create_release == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + if: steps.check.outputs.create_release == 'true' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker images + if: steps.check.outputs.create_release == 'true' + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + rcourtman/pulse:v${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }} + rcourtman/pulse:rc + + - name: Create Release Archive + if: steps.check.outputs.create_release == 'true' + run: | + # Create release tarball + tar -czf pulse-v${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }}.tar.gz \ + --exclude=node_modules \ + --exclude=.git \ + --exclude=.env \ + --exclude=data \ + --exclude=*.log \ + --exclude=temp-release \ + server src scripts package.json package-lock.json README.md LICENSE CHANGELOG.md \ + docker-compose.yml Dockerfile + + - name: Create RC Release + if: steps.check.outputs.create_release == 'true' + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }} + name: v${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }} + prerelease: true + generate_release_notes: true + files: pulse-v${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }}.tar.gz + body: | + ## ๐Ÿงช Release Candidate ${{ steps.check.outputs.rc_number }} + + This is a release candidate for testing. Please report any issues you find. + + ### ๐Ÿ“ฆ Installation Options + + #### Script Install (Recommended) + ```bash + wget -qO- https://github.com/rcourtman/Pulse/releases/download/v${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }}/install-pulse.sh | bash + ``` + + #### Docker + ```bash + docker pull rcourtman/pulse:v${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }} + # or use the rolling RC tag + docker pull rcourtman/pulse:rc + ``` + + ### โš ๏ธ Testing Notes + - This is a pre-release for testing only + - Not recommended for production use + - Please report issues on GitHub + - The `:rc` Docker tag always points to the latest RC \ No newline at end of file diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml new file mode 100644 index 000000000..97a944f03 --- /dev/null +++ b/.github/workflows/stable-release.yml @@ -0,0 +1,306 @@ +name: Automated Stable Release + +on: + push: + branches: + - main + pull_request: + types: [closed] + branches: + - main + +permissions: + contents: write + packages: write + pull-requests: read + +jobs: + detect-stable-release: + runs-on: ubuntu-latest + outputs: + should-release: ${{ steps.check.outputs.should-release }} + suggested-version: ${{ steps.analyze.outputs.suggested-version }} + bump-type: ${{ steps.analyze.outputs.bump-type }} + reasoning: ${{ steps.analyze.outputs.reasoning }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for commit analysis + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check if stable release should be triggered + id: check + run: | + # Check if this is a push to main that came from develop + if [ "${{ github.event_name }}" = "push" ]; then + # Check if last commit message indicates merge from develop + LAST_COMMIT=$(git log -1 --pretty=format:"%s") + echo "Last commit: $LAST_COMMIT" + + if echo "$LAST_COMMIT" | grep -E "(Merge.*develop|Merge pull request.*develop)"; then + echo "should-release=true" >> $GITHUB_OUTPUT + echo "โœ… Detected merge from develop to main - triggering stable release" + else + echo "should-release=false" >> $GITHUB_OUTPUT + echo "โ„น๏ธ Not a develop merge - skipping stable release" + fi + elif [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.action }}" = "closed" ] && [ "${{ github.event.pull_request.merged }}" = "true" ]; then + # PR was merged - check if it was from develop + if [ "${{ github.event.pull_request.head.ref }}" = "develop" ]; then + echo "should-release=true" >> $GITHUB_OUTPUT + echo "โœ… PR from develop was merged - triggering stable release" + else + echo "should-release=false" >> $GITHUB_OUTPUT + echo "โ„น๏ธ PR not from develop - skipping stable release" + fi + else + echo "should-release=false" >> $GITHUB_OUTPUT + echo "โ„น๏ธ Event does not trigger stable release" + fi + + - name: Analyze commits for version bump + id: analyze + if: steps.check.outputs.should-release == 'true' + run: | + # Use our enhanced versionUtils to analyze commits + node -e " + const { analyzeCommitsForVersionBump } = require('./server/versionUtils'); + const analysis = analyzeCommitsForVersionBump(); + + console.log('๐Ÿ“Š Version Analysis:'); + console.log('Current stable version:', analysis.currentStableVersion); + console.log('Suggested version:', analysis.suggestedVersion); + console.log('Bump type:', analysis.bumpType); + console.log('Reasoning:', analysis.reasoning); + console.log('Total commits:', analysis.totalCommits); + + console.log('\\n๐Ÿ“ Commit breakdown:'); + console.log('Breaking changes:', analysis.analysis.breaking.length); + console.log('Features:', analysis.analysis.features.length); + console.log('Fixes:', analysis.analysis.fixes.length); + console.log('Other:', analysis.analysis.other.length); + + // Set outputs for next job + const fs = require('fs'); + const output = fs.readFileSync(process.env.GITHUB_OUTPUT, 'utf8'); + fs.writeFileSync(process.env.GITHUB_OUTPUT, output + + 'suggested-version=' + analysis.suggestedVersion + '\\n' + + 'bump-type=' + analysis.bumpType + '\\n' + + 'reasoning=' + analysis.reasoning + '\\n' + ); + " + + create-stable-release: + needs: detect-stable-release + if: needs.detect-stable-release.outputs.should-release == 'true' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Configure Git + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + - name: Update version and create release + env: + NEW_VERSION: ${{ needs.detect-stable-release.outputs.suggested-version }} + BUMP_TYPE: ${{ needs.detect-stable-release.outputs.bump-type }} + REASONING: ${{ needs.detect-stable-release.outputs.reasoning }} + run: | + echo "๐Ÿš€ Creating stable release v$NEW_VERSION" + echo "๐Ÿ“ˆ Version bump: $BUMP_TYPE" + echo "๐Ÿ’ก Reasoning: $REASONING" + + # Update package.json to stable version + npm version $NEW_VERSION --no-git-tag-version + + # Run tests to ensure everything works + echo "๐Ÿงช Running tests..." + npm test || echo "โš ๏ธ Tests failed but continuing with release" + + # Build CSS + echo "๐ŸŽจ Building CSS..." + npm run build:css || echo "โš ๏ธ CSS build failed but continuing" + + # Commit version bump + git add package.json package-lock.json + git commit -m "chore: release v$NEW_VERSION + + $REASONING + + This stable release includes all changes from the develop branch. + + ๐Ÿค– Generated by automated stable release workflow" + + # Create and push tag + git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" + git push origin main + git push origin "v$NEW_VERSION" + + - name: Generate changelog + id: changelog + env: + NEW_VERSION: ${{ needs.detect-stable-release.outputs.suggested-version }} + run: | + # Get the previous stable tag + PREV_TAG=$(git tag -l "v*" | grep -v "rc\|alpha\|beta" | sort -V | tail -2 | head -1) + if [ -z "$PREV_TAG" ]; then + PREV_TAG="v0.0.0" + fi + + echo "๐Ÿ“ Generating changelog from $PREV_TAG to v$NEW_VERSION" + + # Analyze commits for changelog + node -e " + const { execSync } = require('child_process'); + const { analyzeCommitsForVersionBump } = require('./server/versionUtils'); + + try { + const analysis = analyzeCommitsForVersionBump(); + + let changelog = '## What\\'s Changed\\n\\n'; + + if (analysis.analysis.breaking.length > 0) { + changelog += '### ๐Ÿ’ฅ Breaking Changes\\n'; + analysis.analysis.breaking.forEach(commit => { + changelog += '- ' + commit + '\\n'; + }); + changelog += '\\n'; + } + + if (analysis.analysis.features.length > 0) { + changelog += '### โœจ New Features\\n'; + analysis.analysis.features.forEach(commit => { + changelog += '- ' + commit + '\\n'; + }); + changelog += '\\n'; + } + + if (analysis.analysis.fixes.length > 0) { + changelog += '### ๐Ÿ› Bug Fixes\\n'; + analysis.analysis.fixes.forEach(commit => { + changelog += '- ' + commit + '\\n'; + }); + changelog += '\\n'; + } + + if (analysis.analysis.other.length > 0) { + changelog += '### ๐Ÿ”ง Other Changes\\n'; + analysis.analysis.other.forEach(commit => { + changelog += '- ' + commit + '\\n'; + }); + changelog += '\\n'; + } + + changelog += '### ๐Ÿ“Š Release Statistics\\n'; + changelog += '- **Version bump**: ' + analysis.bumpType + '\\n'; + changelog += '- **Total commits**: ' + analysis.totalCommits + '\\n'; + changelog += '- **Breaking changes**: ' + analysis.analysis.breaking.length + '\\n'; + changelog += '- **New features**: ' + analysis.analysis.features.length + '\\n'; + changelog += '- **Bug fixes**: ' + analysis.analysis.fixes.length + '\\n'; + changelog += '\\n'; + changelog += '### ๐Ÿณ Docker\\n'; + changelog += '\\`\\`\\`bash\\n'; + changelog += 'docker pull rcourtman/pulse:v' + analysis.suggestedVersion + '\\n'; + changelog += 'docker pull rcourtman/pulse:latest\\n'; + changelog += '\\`\\`\\`\\n'; + changelog += '\\n'; + changelog += '๐Ÿค– *This release was automatically created from the develop branch*'; + + console.log(changelog); + + // Write to file for GitHub release + require('fs').writeFileSync('CHANGELOG.md', changelog); + } catch (error) { + console.error('Error generating changelog:', error); + require('fs').writeFileSync('CHANGELOG.md', 'Automated stable release\\n\\nSee commit history for details.'); + } + " + + - name: Build release tarball + env: + NEW_VERSION: ${{ needs.detect-stable-release.outputs.suggested-version }} + run: | + echo "๐Ÿ“ฆ Building release tarball..." + + # Create tarball with all necessary files + tar -czf "pulse-v$NEW_VERSION.tar.gz" \ + --exclude=node_modules \ + --exclude=.git \ + --exclude=.env \ + --exclude=data \ + --exclude=*.log \ + --exclude=temp-release \ + server src/public package.json package-lock.json README.md LICENSE CHANGELOG.md scripts/install-pulse.sh + + ls -lh "pulse-v$NEW_VERSION.tar.gz" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker images + env: + NEW_VERSION: ${{ needs.detect-stable-release.outputs.suggested-version }} + run: | + echo "๐Ÿณ Building multi-arch Docker images..." + + # Build and push stable version tag and latest tag + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag "rcourtman/pulse:v$NEW_VERSION" \ + --tag "rcourtman/pulse:latest" \ + --push . + + echo "โœ… Docker images pushed successfully" + + - name: Create GitHub Release + env: + NEW_VERSION: ${{ needs.detect-stable-release.outputs.suggested-version }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "๐Ÿ“ Creating GitHub release..." + + # Create the release + gh release create "v$NEW_VERSION" \ + --title "Release v$NEW_VERSION" \ + --notes-file CHANGELOG.md \ + "pulse-v$NEW_VERSION.tar.gz" + + echo "โœ… Release v$NEW_VERSION created successfully!" + echo "๐Ÿ”— https://github.com/${{ github.repository }}/releases/tag/v$NEW_VERSION" + + - name: Cleanup + run: | + rm -f CHANGELOG.md pulse-v*.tar.gz \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f3a361af..51b4d7d14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,14 @@ Thank you for your interest in contributing to Pulse! We appreciate your help. Here are some guidelines to follow: +## Branch Strategy + +Pulse uses a two-branch workflow: +- **`main`** - Stable releases only (protected) +- **`develop`** - Daily development work (default working branch) + +All contributions should target the `develop` branch. + ## Reporting Bugs - Please ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/rcourtman/Pulse/issues). @@ -16,11 +24,45 @@ Thank you for your interest in contributing to Pulse! We appreciate your help. H ## Pull Requests -- Fork the repository and create your branch from `main`. -- Ensure your code adheres to the project's existing style. -- If you've added code that should be tested, add tests. -- Ensure the test suite passes (if applicable). -- Make sure your code lints (if linters are set up). -- Issue that pull request! +### Getting Started +1. **Fork the repository** and clone your fork locally +2. **Create your branch from `develop`**: `git checkout -b feature/your-feature develop` +3. **Set up development environment**: + ```bash + npm install + npm run build:css + npm run dev # Starts development server with hot reload + ``` -We will review your pull request and provide feedback. Thank you for your contribution! \ No newline at end of file +### Development Workflow +- **Local testing**: Your changes will show with dynamic RC versions (e.g., "3.24.0-rc5") +- **Version display**: RC versions increment automatically with each commit +- **No version management needed**: The system handles versioning automatically + +### Before Submitting +- Ensure your code adheres to the project's existing style +- If you've added code that should be tested, add tests +- Ensure the test suite passes: `npm test` +- Make sure your code lints (if linters are set up) +- Test your changes thoroughly + +### Submitting Your Pull Request +1. **Push to your fork**: `git push origin feature/your-feature` +2. **Create a pull request** targeting the `develop` branch +3. **Provide a clear description** of what your changes do +4. **Reference any related issues** in your PR description + +### After Submission +- We will review your pull request and provide feedback +- Your changes will automatically get RC releases for testing when merged to `develop` +- Once tested and approved, changes will be included in the next stable release + +## Release Candidate Testing + +When your PR is merged to `develop`: +- **Automatic RC creation**: A new RC release is created automatically +- **Docker images**: Multi-arch Docker images are built and published +- **Version tracking**: RC versions increment automatically (rc1, rc2, rc3...) +- **Testing opportunity**: Community can test your changes before stable release + +Thank you for your contribution! \ No newline at end of file diff --git a/DEVELOPMENT_WORKFLOW.md b/DEVELOPMENT_WORKFLOW.md new file mode 100644 index 000000000..89506aa97 --- /dev/null +++ b/DEVELOPMENT_WORKFLOW.md @@ -0,0 +1,69 @@ +# Pulse Development Workflow + +## Branch Structure + +- **`main`** - Stable releases only. Users install from here. +- **`develop`** - Daily development work. RC releases are created from here. + +## Daily Workflow + +### 1. Always work on develop branch +```bash +git checkout develop +git pull origin develop +``` + +### 2. Make your changes and commit frequently +```bash +# Edit files... +git add . +git commit -m "fix: your change description" +git push +``` + +### 3. RC releases are automatic +- Every push to `develop` creates a new RC release (if there are changes) +- Share RC version with users who need to test: `v3.24.0-rc1`, `v3.24.0-rc2`, etc. + +### 4. Creating a stable release +When RC testing is complete and you're ready for a stable release: + +```bash +# 1. Ensure develop is up to date +git checkout develop +git pull + +# 2. Merge to main +git checkout main +git merge develop + +# 3. Update version in package.json (remove any -rc suffix) +# Edit package.json to bump version if needed + +# 4. Commit and tag +git add package.json +git commit -m "chore: release v3.25.0" +git tag v3.25.0 +git push origin main --tags + +# 5. Go back to develop for daily work +git checkout develop +git merge main # Keep develop in sync +git push +``` + +## For Users Reporting Issues + +1. User reports issue on v3.24.0 +2. You fix it on `develop` branch +3. Automatic RC is created (e.g., v3.24.1-rc1) +4. Ask user to test: "Can you test with v3.24.1-rc1?" +5. If good, merge to main for stable v3.24.1 + +## Key Benefits + +- โœ… Stable users stay on tested versions +- โœ… You can commit frequently without affecting stable users +- โœ… Easy testing workflow with automatic RC builds +- โœ… Professional release management +- โœ… Clear separation between development and production \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 36c2466ca..5473d759b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ---- Builder Stage ---- -FROM node:18-alpine AS builder +FROM node:20-alpine AS builder WORKDIR /usr/src/app @@ -24,7 +24,7 @@ RUN npm run build:css RUN npm prune --production # ---- Runner Stage ---- -FROM node:18-alpine +FROM node:20-alpine WORKDIR /usr/src/app diff --git a/README.md b/README.md index d6b1a77d9..ed45b82d4 100644 --- a/README.md +++ b/README.md @@ -709,20 +709,54 @@ To update a tarball installation: If running from source code: +**For stable releases (production):** ```bash cd /path/to/pulse +git checkout main git pull origin main npm install npm run build:css npm run start # or your preferred restart method ``` -**Note:** The development setup only requires npm install in the root directory, not in a separate server directory. +**For development/RC versions:** +```bash +cd /path/to/pulse +git checkout develop +git pull origin develop +npm install +npm run build:css +npm run start # or your preferred restart method +``` + +**Note:** +- The development setup only requires npm install in the root directory, not in a separate server directory. +- The `develop` branch shows dynamic RC versions (e.g., "3.24.0-rc5") that auto-increment with each commit. +- The `main` branch contains stable releases only. ## ๐Ÿ“ Contributing Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md). +### Development Workflow + +**Branch Strategy:** +- `main` - Stable releases only (protected) +- `develop` - Daily development work (default working branch) + +**Release Candidate (RC) Automation:** +- Every commit to `develop` automatically creates an RC release +- RC versions increment automatically: `v3.24.0-rc1`, `v3.24.0-rc2`, etc. +- Docker images are built for both `amd64` and `arm64` architectures +- Local development shows dynamic RC versions that update with each commit + +**Making Contributions:** +1. Fork the repository +2. Create a feature branch from `develop` +3. Make your changes +4. Test locally (version will show as RC automatically) +5. Submit a pull request to `develop` + ## ๐Ÿ”’ Privacy * **No Data Collection:** Pulse does not collect or transmit any telemetry or user data externally. diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md new file mode 100644 index 000000000..d3722bc67 --- /dev/null +++ b/RELEASE_GUIDE.md @@ -0,0 +1,419 @@ +# Release Guide for Pulse + +A comprehensive guide for handling commits, releases, and development workflow. + +## Branch Strategy + +**IMPORTANT**: Pulse uses a two-branch workflow: +- `main` - Stable releases only (protected) +- `develop` - Daily development work (default working branch) + +Always check current branch: `git branch --show-current` + +## Versioning System + +**Dynamic RC Versioning:** +- **Develop branch**: Automatically calculates RC versions from git commits (e.g., "3.24.0-rc5") +- **Main branch**: Uses stable package.json version (e.g., "3.24.0") +- **Package.json**: Always contains base stable version, never RC versions +- **Local display**: Shows dynamic RC version when on develop branch +- **Automatic sync**: RC versions increment with each commit, no manual management needed + +## Pre-flight Checklist + +Before starting any release process, verify: +- [ ] Clean working directory: `git status` +- [ ] Docker logged in: `docker login` (check with `docker info | grep Username`) +- [ ] GitHub CLI authenticated: `gh auth status` +- [ ] Docker buildx available: `docker buildx ls || docker buildx create --name mybuilder --use` +- [ ] All tests passing: `npm test` + +## Prerequisites Check + +First run (only if user mentions "clean context" or you see errors): + +```bash +# Check required tools +which git || echo "ERROR: git not installed" +which node || echo "ERROR: Node.js not installed" +which npm || echo "ERROR: npm not installed" +which gh || echo "WARNING: GitHub CLI not installed (needed for releases)" +which docker || echo "WARNING: Docker not installed (needed for Docker releases)" + +# If in a fresh clone, run: +npm install +``` + +## Quick Reference + +When user says: +- **"commit this"** โ†’ Check branch first! Then go to [Commit Process](#commit-process) +- **"create a release"** โ†’ Use [Automated Stable Release Process](#automated-stable-release-process-new) via PR +- **"create an RC"** or **"release candidate"** โ†’ Automatic from `develop` push (see below) +- **"what changed?"** โ†’ `git status -s` and `git diff --stat` +- **"run tests"** โ†’ `npm test` (also check for lint/typecheck scripts) +- **"merge to main"** โ†’ Use [Automated Stable Release Process](#automated-stable-release-process-new) +- **"manual release"** โ†’ Only for hotfixes โ†’ [Manual Release Process](#manual-release-process-legacy) + +## Commit Process + +### 1. Check Branch & Summarize +```bash +# CRITICAL: Check current branch +git branch --show-current +# If not on develop: git checkout develop +git status -s +git diff --stat +``` +Tell user: "You're on [branch]. You've modified X files. Main changes: [brief summary]" + +### 2. Stage & Commit +```bash +git add . +git commit -m ": " +``` + +**Commit types**: `feat:` (new feature), `fix:` (bug fix), `docs:`, `chore:`, `refactor:` + +### 3. Push +```bash +# For daily work (most common) +git push origin develop # This triggers automatic RC release! + +# For hotfixes on main (rare) +git push origin main +``` + +**If rejected**: +```bash +git pull --rebase origin develop # or main +git push origin develop # or main +``` + +## Pre-Release Process (Automatic) + +**NEW**: RC releases are now fully automated when you push to `develop` branch! + +### How It Works +1. Make changes on `develop` branch +2. Commit and push: `git push origin develop` +3. GitHub Actions automatically: + - Calculates new RC version (increments from last RC) + - Updates package.json with new RC version + - Commits version bump back to develop + - Creates RC release with proper versioning + - Builds multi-arch Docker images + +### Dynamic Local Versioning +- **Local develop branch**: Shows dynamic RC versions calculated from git commits +- **Example**: If you have 5 commits since v3.24.0, version shows as "3.24.0-rc5" +- **Auto-increment**: Each new commit increments the RC number instantly +- **No manual version management**: Version automatically stays in sync + +### Manual RC Release (if needed) +Only use if automatic process fails or for special cases: +```bash +# On develop branch +git tag v3.24.0-rc1 +git push origin v3.24.0-rc1 +gh release create v3.24.0-rc1 --title "v3.24.0-rc1" --prerelease --generate-release-notes +``` + +### Docker Images for RC +The GitHub Action handles Docker builds automatically, including: +- Multi-arch builds (amd64, arm64) +- Tagged with RC version +- Does NOT update `:latest` tag +- Rolling `:rc` tag always points to latest RC + +## Automated Stable Release Process (NEW!) + +**IMPORTANT**: Stable releases are now fully automated when you merge develop to main! + +### How It Works +1. When develop branch is ready for stable release, create a pull request from develop to main +2. Once the PR is approved and merged, GitHub Actions automatically: + - Analyzes all commits since the last stable release using semantic versioning + - Determines appropriate version bump (major/minor/patch) + - Updates package.json with the new stable version + - Creates a git tag and GitHub release + - Builds multi-arch Docker images with both version tag and `:latest` + - Generates comprehensive changelog from commit analysis + +### Semantic Commit Analysis +The automation analyzes commit messages to determine version bumps: +- **Major bump**: Commits with `BREAKING CHANGE` or `!:` in message +- **Minor bump**: Commits starting with `feat:` or `feature:` +- **Patch bump**: Commits starting with `fix:` or `bugfix:` +- **Patch bump**: All other commits (chore, docs, refactor, etc.) + +### Creating a Stable Release +```bash +# 1. Ensure develop is ready for release +git checkout develop +git pull + +# 2. Create pull request to main (preferred method) +gh pr create --base main --head develop --title "Release v3.25.0" --body "Ready for stable release + +This PR includes: +- 8 new features +- 5 bug fixes +- Various improvements + +The automated workflow will analyze commits and create the appropriate version bump." + +# 3. Get PR approved and merge it +# 4. GitHub Actions automatically handles the rest! +``` + +### Alternative: Direct Merge (if needed) +```bash +# 1. Ensure develop is up to date +git checkout develop +git pull + +# 2. Switch to main and merge +git checkout main +git pull +git merge develop + +# 3. Push (this triggers automated stable release) +git push origin main + +# GitHub Actions will detect the merge and create the stable release automatically +``` + +## Manual Release Process (Legacy) + +**NOTE**: Manual releases are now rarely needed since the automated stable release process handles most cases. Use this only for hotfixes or special circumstances. + +**IMPORTANT**: Manual releases must be done from `main` branch only! + +### 1. Pre-release Verification +```bash +# Verify on main branch +git branch --show-current # Must show "main" +# If not: echo "ERROR: Must be on main branch for releases!" + +# Verify pre-flight checklist items +git status # Should be clean +docker info | grep Username || echo "WARNING: Not logged into Docker Hub" +gh auth status || echo "ERROR: GitHub CLI not authenticated. Run: gh auth login" +docker buildx ls || echo "WARNING: Docker buildx not available" +``` + +### 2. Analyze Changes +```bash +# Get current version (should be stable base version like "3.24.0") +node -p "require('./package.json').version" + +# Get dynamic version from API if running locally +curl -s http://localhost:7655/api/version | grep -o '"version":"[^"]*"' | cut -d'"' -f4 + +# Analyze commits since last stable tag +git log $(git describe --tags --abbrev=0)..HEAD --oneline +``` + +### 3. Determine Version +- Breaking changes โ†’ Major (1.0.0 โ†’ 2.0.0) +- New features โ†’ Minor (1.0.0 โ†’ 1.1.0) +- Bug fixes โ†’ Patch (1.0.0 โ†’ 1.0.1) +- Testing release โ†’ RC: X.Y.Z-rc.N (release candidate) + +Ask user: "Based on changes, I suggest version X.Y.Z. OK?" + +### 4. Update Version & Run Tests + +**IMPORTANT**: For stable releases, package.json should contain the base stable version (e.g., "3.24.0"), not RC versions. The dynamic versioning system handles RC display automatically. + +```bash +# Update package.json and package-lock.json to stable version +npm version X.Y.Z --no-git-tag-version + +# Alternatively, if npm version fails: +node -e "const p=require('./package.json');p.version='X.Y.Z';require('fs').writeFileSync('./package.json',JSON.stringify(p,null,2)+'\n')" +npm install # This updates package-lock.json + +# Run tests +npm test + +# Check for deprecation warnings +npm audit || echo "Check audit results - warnings don't block release" + +# Build CSS +npm run build:css + +# Run linting if available +npm run lint || echo "No lint script found" + +# Run type checking if available +npm run typecheck || echo "No typecheck script found" +``` + +**If tests fail**: Try to fix or ask user how to proceed + +### 5. Commit & Tag +```bash +git add package.json package-lock.json +git commit -m "chore: release vX.Y.Z" +git push origin main + +git tag -a vX.Y.Z -m "Release vX.Y.Z" +git push origin vX.Y.Z +``` + +### 6. Build Release Tarball & Create Changelog + +**IMPORTANT CHANGELOG RULES**: +- Analyze ALL changes since last release: `git diff v[last]..v[current] --stat` +- Focus ONLY on user-visible changes that matter to users +- DO NOT include: dependency updates, docs changes, gitignore, dev tooling, Docker file restoration +- DO NOT repeat version number in changelog (GitHub already shows it) +- Use `echo` instead of heredoc to avoid EOF issues +- Look for big picture changes: major refactors, feature removals, new functionality + +```bash +# First understand the scope of changes +git diff $(git describe --tags --abbrev=0)..HEAD --stat +echo "Total changes: $(git diff $(git describe --tags --abbrev=0)..HEAD --stat | tail -1)" + +# Create user-focused changelog - use echo, NOT heredoc +echo "## Changes +- [List only user-visible changes] +- [Major feature additions/removals] +- [Breaking changes] + +This release [brief summary of main theme]." > CHANGELOG_TEMP.md + +# Build tarball +echo "X.Y.Z" | ./scripts/create-release.sh + +# If create-release.sh fails, manual fallback: +if [ ! -f pulse-vX.Y.Z.tar.gz ]; then + echo "Release script failed, creating tarball manually..." + tar -czf pulse-vX.Y.Z.tar.gz \ + --exclude=node_modules \ + --exclude=.git \ + --exclude=.env \ + --exclude=data \ + --exclude=*.log \ + server src/public package.json package-lock.json README.md LICENSE CHANGELOG.md +fi +``` + +### 7. Build and Test Docker Images +```bash +# Ensure buildx is available +docker buildx ls || docker buildx create --name mybuilder --use + +# IMPORTANT: First check what port the app uses (usually 7655, not 3000!) +grep "const PORT" server/index.js || grep "Server listening on port" server/index.js + +# Build single-platform image for testing +docker build -t rcourtman/pulse:vX.Y.Z . + +# Test with real .env file if available (RECOMMENDED) +if [ -f .env ]; then + echo "Testing with real .env configuration..." + docker run --rm -d --name pulse-test --env-file .env -p 7656:7655 rcourtman/pulse:vX.Y.Z +else + echo "Testing with minimal config (less thorough)..." + docker run --rm -d --name pulse-test \ + -e PROXMOX_HOST=test.example.com \ + -e PROXMOX_TOKEN_ID=test@pam!test \ + -e PROXMOX_TOKEN_SECRET=test-secret \ + -p 7656:7655 rcourtman/pulse:vX.Y.Z +fi + +sleep 5 # Give it time to start + +# Check if container is running +docker ps | grep pulse-test || (echo "Container failed to start!"; docker logs pulse-test; exit 1) + +# Check container logs for successful startup +docker logs pulse-test 2>&1 | grep "Server listening on port" || (echo "Server didn't start!"; docker logs pulse-test; exit 1) + +# If using real config, verify data collection +if [ -f .env ]; then + docker logs pulse-test 2>&1 | grep -E "(nodes:|VMs:|CTs:)" && echo "โœ“ Data collection working" +fi + +# Try to access the main page +curl -s -o /dev/null -w "%{http_code}" http://localhost:7656/ | grep -q "200" && echo "โœ“ Web interface accessible" + +# Check for API health (if endpoint exists) +curl -s http://localhost:7656/api/health 2>/dev/null | grep -q "ok" && echo "โœ“ API health check passed" || echo "Note: API health endpoint may not exist in this version" + +# Check logs for critical errors (ignore connection errors if using test config) +docker logs pulse-test 2>&1 | grep -i error | grep -v "ENOTFOUND\|ECONNREFUSED\|getaddrinfo" | head -5 + +# Stop test container +docker stop pulse-test +echo "โœ“ Docker test completed successfully" + +# If tests pass, build and push multi-arch images +echo "Building and pushing multi-arch images..." +docker buildx build --platform linux/amd64,linux/arm64 -t rcourtman/pulse:vX.Y.Z -t rcourtman/pulse:latest --push . + +# Verify images were pushed +docker manifest inspect rcourtman/pulse:vX.Y.Z || echo "WARNING: Failed to verify Docker push" +``` + +**Important Notes**: +- The app runs on port 7655, not 3000! +- Always test with real .env file when available for better validation +- Connection errors are expected with test config, but not with real config +- Main page (/) is more reliable to test than /api/health endpoint (which may not exist) + +### 8. Create GitHub Release +```bash +# Verify GitHub CLI is authenticated +gh auth status || (echo "ERROR: Must authenticate with GitHub CLI first: gh auth login"; exit 1) + +# Create GitHub release with tarball +gh release create vX.Y.Z --title "Release vX.Y.Z" --notes-file CHANGELOG_TEMP.md pulse-vX.Y.Z.tar.gz + +# Clean up temporary files +rm -f CHANGELOG_TEMP.md pulse-vX.Y.Z.tar.gz +``` + +### 9. Post-Release +```bash +# Verify the release was created +gh release view vX.Y.Z + +# Check Docker Hub for new images +docker manifest inspect rcourtman/pulse:vX.Y.Z +docker manifest inspect rcourtman/pulse:latest + +echo "โœ… Release vX.Y.Z completed successfully!" +echo "๐Ÿณ Docker: docker pull rcourtman/pulse:vX.Y.Z" +echo "๐Ÿ“ฆ GitHub: https://github.com/rcourtman/pulse/releases/tag/vX.Y.Z" +``` + +## Error Recovery + +### If Docker push fails: +```bash +# Re-authenticate +docker login +# Retry the build and push +docker buildx build --platform linux/amd64,linux/arm64 -t rcourtman/pulse:vX.Y.Z -t rcourtman/pulse:latest --push . +``` + +### If GitHub release fails: +```bash +# Delete failed release if it was created +gh release delete vX.Y.Z --yes +# Try again +gh release create vX.Y.Z --title "Release vX.Y.Z" --notes-file CHANGELOG_TEMP.md pulse-vX.Y.Z.tar.gz +``` + +### If git push is rejected: +```bash +git pull --rebase origin main +# Resolve any conflicts, then: +git push origin main +git push origin vX.Y.Z +``` \ No newline at end of file diff --git a/package.json b/package.json index 2adc30f3d..cdf15917f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse", - "version": "3.24.0", + "version": "3.24.0-rc6", "description": "A lightweight monitoring application for Proxmox VE.", "main": "server/index.js", "scripts": { diff --git a/scripts/install-pulse.sh b/scripts/install-pulse.sh index 3aff8de3d..56811c9ce 100755 --- a/scripts/install-pulse.sh +++ b/scripts/install-pulse.sh @@ -415,6 +415,17 @@ download_and_extract_tarball() { # Get current installed version get_current_version() { + # Try to get version from the running service API first + if systemctl is-active --quiet pulse.service 2>/dev/null; then + local api_version + api_version=$(curl -s -m 5 "http://localhost:7655/api/version" 2>/dev/null | grep -o '"version":"[^"]*"' | cut -d'"' -f4 2>/dev/null) + if [ -n "$api_version" ] && [ "$api_version" != "null" ]; then + echo "$api_version" + return 0 + fi + fi + + # Fallback to package.json if API is not available if [ -f "$PULSE_DIR/package.json" ]; then grep '"version"' "$PULSE_DIR/package.json" | head -1 | sed -E 's/.*"version": "([^"]+)".*/\1/' fi diff --git a/server/index.js b/server/index.js index 0cece4259..aa048b4f5 100644 --- a/server/index.js +++ b/server/index.js @@ -729,8 +729,13 @@ app.get('/api/alerts/status', (req, res) => { // Version API endpoint app.get('/api/version', async (req, res) => { try { - const packageJson = require('../package.json'); - const currentVersion = packageJson.version || 'N/A'; + const { getCurrentVersionInfo } = require('./versionUtils'); + + // Get version info using centralized logic + const versionInfo = getCurrentVersionInfo(); + const currentVersion = versionInfo.version; + const gitBranch = versionInfo.gitBranch; + const isDevelopment = versionInfo.isDevelopment; let latestVersion = currentVersion; let updateAvailable = false; @@ -748,7 +753,9 @@ app.get('/api/version', async (req, res) => { res.json({ version: currentVersion, latestVersion: latestVersion, - updateAvailable: updateAvailable + updateAvailable: updateAvailable, + gitBranch: gitBranch, + isDevelopment: isDevelopment }); } catch (error) { console.error("[Version API] Error in version endpoint:", error); @@ -1746,6 +1753,7 @@ async function startServer() { path.join(__dirname, '../src/public'), // Frontend files path.join(__dirname, './'), // Server files path.join(__dirname, '../data'), // Config files + path.join(__dirname, '../package.json'), // Package.json for auto-restart on version updates ]; devWatcher = chokidar.watch(watchPaths, { diff --git a/server/updateManager.js b/server/updateManager.js index 3468f2da8..09d012406 100644 --- a/server/updateManager.js +++ b/server/updateManager.js @@ -5,6 +5,7 @@ const path = require('path'); const { exec, spawn } = require('child_process'); const { promisify } = require('util'); const { getUpdateChannelPreference } = require('./configLoader'); +const { getCurrentVersion } = require('./versionUtils'); const execAsync = promisify(exec); class UpdateManager { @@ -61,20 +62,23 @@ class UpdateManager { * @param {string} channelOverride - Optional channel override ('stable' or 'rc') */ async checkForUpdates(channelOverride = null) { + // Get the current version using centralized logic (outside try block for error handling) + const dynamicCurrentVersion = getCurrentVersion(); + + // Use override channel if provided and valid, otherwise use config + const configChannel = getUpdateChannelPreference(); + const updateChannel = (channelOverride && ['stable', 'rc'].includes(channelOverride)) + ? channelOverride + : configChannel; + let channelDescription = ''; + try { console.log('[UpdateManager] Checking for updates...'); - // Use override channel if provided and valid, otherwise use config - const configChannel = getUpdateChannelPreference(); - const updateChannel = (channelOverride && ['stable', 'rc'].includes(channelOverride)) - ? channelOverride - : configChannel; - if (channelOverride && channelOverride !== configChannel) { console.log(`[UpdateManager] Using channel override: ${channelOverride} (config: ${configChannel})`); } let response; - let channelDescription = ''; if (updateChannel === 'stable') { // Stable channel: only check latest stable release @@ -113,17 +117,20 @@ class UpdateManager { const releaseVersion = release.tag_name.replace('v', ''); const releaseIsRC = this.isReleaseCandidate(releaseVersion); - if (releaseIsRC && semver.gt(releaseVersion, this.currentVersion)) { - latestRelease = release; - break; + if (releaseIsRC) { + // For RC channel, show the latest RC regardless of current version + // This allows showing RC versions even if current is stable + if (!latestRelease || semver.gt(releaseVersion, latestRelease.tag_name.replace('v', ''))) { + latestRelease = release; + } } } if (!latestRelease) { // No newer RC version found const updateInfo = { - currentVersion: this.currentVersion, - latestVersion: this.currentVersion, + currentVersion: dynamicCurrentVersion, + latestVersion: dynamicCurrentVersion, updateAvailable: false, isDocker: this.isDockerEnvironment(), releaseNotes: 'No newer RC version available', @@ -132,7 +139,7 @@ class UpdateManager { assets: [], updateChannel: channelDescription }; - console.log(`[UpdateManager] No RC updates available: ${this.currentVersion}`); + console.log(`[UpdateManager] No RC updates available: ${dynamicCurrentVersion}`); return updateInfo; } @@ -142,21 +149,24 @@ class UpdateManager { const latestVersion = response.data.tag_name.replace('v', ''); // For stable channel, also consider "downgrade" from RC as an update - const isCurrentRC = this.isReleaseCandidate(); + const isCurrentRC = this.isReleaseCandidate(dynamicCurrentVersion); const isStableChannel = updateChannel === 'stable'; - const isDifferentVersion = latestVersion !== this.currentVersion; + const isDifferentVersion = latestVersion !== dynamicCurrentVersion; let updateAvailable; if (isStableChannel && isCurrentRC && isDifferentVersion) { // Offer stable version even if it's older than current RC updateAvailable = true; + } else if (updateChannel === 'rc') { + // For RC channel, show update if versions differ or if latest is newer + updateAvailable = isDifferentVersion || semver.gt(latestVersion, dynamicCurrentVersion); } else { // Normal case: only newer versions - updateAvailable = semver.gt(latestVersion, this.currentVersion); + updateAvailable = semver.gt(latestVersion, dynamicCurrentVersion); } const updateInfo = { - currentVersion: this.currentVersion, + currentVersion: dynamicCurrentVersion, latestVersion, updateAvailable, isDocker: this.isDockerEnvironment(), @@ -171,11 +181,59 @@ class UpdateManager { })) }; - console.log(`[UpdateManager] Current version: ${this.currentVersion}, Latest version: ${latestVersion}, Channel: ${channelDescription}, Docker: ${updateInfo.isDocker}`); + console.log(`[UpdateManager] Current version: ${dynamicCurrentVersion}, Latest version: ${latestVersion}, Channel: ${channelDescription}, Docker: ${updateInfo.isDocker}`); return updateInfo; } catch (error) { console.error('[UpdateManager] Error checking for updates:', error.message); + + // Handle different types of errors gracefully + if (error.response?.status === 403) { + // GitHub API rate limit exceeded + console.warn('[UpdateManager] GitHub API rate limit exceeded, returning current version info'); + return { + currentVersion: dynamicCurrentVersion, + latestVersion: dynamicCurrentVersion, + updateAvailable: false, + isDocker: this.isDockerEnvironment(), + releaseNotes: 'Unable to check for updates: GitHub API rate limit exceeded. Please try again later.', + releaseUrl: null, + publishedAt: null, + assets: [], + updateChannel: channelDescription || 'unknown', + rateLimited: true + }; + } else if (error.response?.status === 404) { + // Repository or release not found + console.warn('[UpdateManager] Repository or release not found'); + return { + currentVersion: dynamicCurrentVersion, + latestVersion: dynamicCurrentVersion, + updateAvailable: false, + isDocker: this.isDockerEnvironment(), + releaseNotes: 'Unable to check for updates: Repository or release not found.', + releaseUrl: null, + publishedAt: null, + assets: [], + updateChannel: channelDescription || 'unknown' + }; + } else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { + // Network connectivity issues + console.warn('[UpdateManager] Network connectivity issues'); + return { + currentVersion: dynamicCurrentVersion, + latestVersion: dynamicCurrentVersion, + updateAvailable: false, + isDocker: this.isDockerEnvironment(), + releaseNotes: 'Unable to check for updates: Network connectivity issues. Please check your internet connection.', + releaseUrl: null, + publishedAt: null, + assets: [], + updateChannel: channelDescription || 'unknown' + }; + } + + // For other errors, still throw but with more context throw new Error(`Failed to check for updates: ${error.message}`); } } diff --git a/server/versionUtils.js b/server/versionUtils.js new file mode 100644 index 000000000..d29164ca3 --- /dev/null +++ b/server/versionUtils.js @@ -0,0 +1,313 @@ +/** + * Centralized version calculation utility + * Used by both /api/version endpoint and UpdateManager to ensure consistency + */ + +const { execSync } = require('child_process'); +const path = require('path'); + +/** + * Calculate the current version dynamically from git + * @returns {Object} Version information including version, branch, and isDevelopment + */ +function getCurrentVersionInfo() { + try { + const packageJson = require('../package.json'); + + let currentVersion = packageJson.version || 'N/A'; + let gitBranch = null; + let isDevelopment = false; + + // Try to detect git branch and calculate dynamic version + try { + const gitDir = path.join(__dirname, '..'); + + // Get current branch + gitBranch = execSync('git branch --show-current', { + cwd: gitDir, + encoding: 'utf8' + }).trim(); + + // If on develop branch, calculate RC version from git + if (gitBranch === 'develop') { + isDevelopment = true; + try { + // Get the latest stable release tag + const latestStableTag = execSync('git tag -l "v*" | grep -v "rc\\\\|alpha\\\\|beta" | sort -V | tail -1', { + cwd: gitDir, + encoding: 'utf8', + shell: '/bin/bash' + }).trim(); + + if (latestStableTag) { + // Remove 'v' prefix to get base version + const baseVersion = latestStableTag.replace(/^v/, ''); + + // Count commits since the latest stable tag + const commitsSince = execSync(`git rev-list --count ${latestStableTag}..HEAD`, { + cwd: gitDir, + encoding: 'utf8' + }).trim(); + + const commitsCount = parseInt(commitsSince, 10); + + if (commitsCount > 0) { + // Calculate RC version: base version + rc + commit count + currentVersion = `${baseVersion}-rc${commitsCount}`; + } else { + // No commits since stable, use base version + currentVersion = baseVersion; + } + } + } catch (versionError) { + console.log('[VersionUtils] Could not calculate RC version from git, using package.json'); + // Fall back to package.json version + currentVersion = packageJson.version; + } + } + } catch (gitError) { + // Git not available or not a git repo + gitBranch = null; + currentVersion = packageJson.version; + } + + return { + version: currentVersion, + gitBranch: gitBranch, + isDevelopment: isDevelopment || gitBranch === 'develop' || process.env.NODE_ENV === 'development' + }; + } catch (error) { + console.warn('[VersionUtils] Error getting current version:', error.message); + const packageJson = require('../package.json'); + return { + version: packageJson.version || 'N/A', + gitBranch: null, + isDevelopment: false + }; + } +} + +/** + * Get just the version string (for backwards compatibility) + * @returns {string} The current version + */ +function getCurrentVersion() { + return getCurrentVersionInfo().version; +} + +/** + * Analyze commits since the last stable release to suggest version bump + * @returns {Object} Analysis with suggested version bump and reasoning + */ +function analyzeCommitsForVersionBump() { + try { + const { execSync } = require('child_process'); + const packageJson = require('../package.json'); + const gitDir = path.join(__dirname, '..'); + + // Get the latest stable release tag (no RC/alpha/beta) + let latestStableTag; + try { + latestStableTag = execSync('git tag -l "v*" | grep -v "rc\\|alpha\\|beta" | sort -V | tail -1', { + cwd: gitDir, + encoding: 'utf8', + shell: '/bin/bash' + }).trim(); + } catch (error) { + // No stable tags found, use v0.0.0 as baseline + latestStableTag = 'v0.0.0'; + } + + if (!latestStableTag) { + latestStableTag = 'v0.0.0'; + } + + // Get commit messages since last stable release + let commitMessages; + try { + commitMessages = execSync(`git log ${latestStableTag}..HEAD --pretty=format:"%s"`, { + cwd: gitDir, + encoding: 'utf8' + }).trim(); + } catch (error) { + // If git log fails, assume no commits + commitMessages = ''; + } + + if (!commitMessages) { + return { + currentStableVersion: latestStableTag.replace(/^v/, ''), + suggestedVersion: packageJson.version, + bumpType: 'none', + reasoning: 'No commits since last stable release', + commits: [] + }; + } + + const commits = commitMessages.split('\n').filter(msg => msg.trim()); + + // Analyze commit types + const analysis = { + breaking: [], + features: [], + fixes: [], + other: [] + }; + + commits.forEach(commit => { + const msg = commit.toLowerCase(); + + // Check for breaking changes + if (commit.includes('BREAKING CHANGE') || commit.includes('!:')) { + analysis.breaking.push(commit); + } + // Check for features + else if (msg.startsWith('feat:') || msg.startsWith('feature:')) { + analysis.features.push(commit); + } + // Check for fixes + else if (msg.startsWith('fix:') || msg.startsWith('bugfix:')) { + analysis.fixes.push(commit); + } + // Everything else + else { + analysis.other.push(commit); + } + }); + + // Determine version bump type + let bumpType = 'patch'; + let reasoning = ''; + + if (analysis.breaking.length > 0) { + bumpType = 'major'; + reasoning = `Major bump due to ${analysis.breaking.length} breaking change(s)`; + } else if (analysis.features.length > 0) { + bumpType = 'minor'; + reasoning = `Minor bump due to ${analysis.features.length} new feature(s)`; + } else if (analysis.fixes.length > 0) { + bumpType = 'patch'; + reasoning = `Patch bump due to ${analysis.fixes.length} bug fix(es)`; + } else { + bumpType = 'patch'; + reasoning = `Patch bump for ${analysis.other.length} other change(s)`; + } + + // Calculate suggested version + const currentStableVersion = latestStableTag.replace(/^v/, ''); + const suggestedVersion = calculateNextVersion(currentStableVersion, bumpType); + + return { + currentStableVersion, + suggestedVersion, + bumpType, + reasoning, + commits: commits, + analysis, + totalCommits: commits.length + }; + + } catch (error) { + console.warn('[VersionUtils] Error analyzing commits for version bump:', error.message); + const packageJson = require('../package.json'); + return { + currentStableVersion: packageJson.version, + suggestedVersion: packageJson.version, + bumpType: 'none', + reasoning: 'Error analyzing commits', + commits: [] + }; + } +} + +/** + * Calculate the next version based on current version and bump type + * @param {string} currentVersion - Current semantic version (e.g., "3.24.0") + * @param {string} bumpType - Type of bump: major, minor, or patch + * @returns {string} Next version + */ +function calculateNextVersion(currentVersion, bumpType) { + try { + // Parse current version + const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/); + if (!versionMatch) { + throw new Error(`Invalid version format: ${currentVersion}`); + } + + let [, major, minor, patch] = versionMatch.map(Number); + + switch (bumpType) { + case 'major': + major += 1; + minor = 0; + patch = 0; + break; + case 'minor': + minor += 1; + patch = 0; + break; + case 'patch': + patch += 1; + break; + default: + // No bump + break; + } + + return `${major}.${minor}.${patch}`; + } catch (error) { + console.warn('[VersionUtils] Error calculating next version:', error.message); + return currentVersion; + } +} + +/** + * Check if current branch should trigger a stable release + * (i.e., we're on main branch and last commit was a merge from develop) + * @returns {boolean} True if this should trigger a stable release + */ +function shouldTriggerStableRelease() { + try { + const { execSync } = require('child_process'); + const gitDir = path.join(__dirname, '..'); + + // Check if we're on main branch + const currentBranch = execSync('git branch --show-current', { + cwd: gitDir, + encoding: 'utf8' + }).trim(); + + if (currentBranch !== 'main') { + return false; + } + + // Check if the last commit was a merge from develop + try { + const lastCommitMessage = execSync('git log -1 --pretty=format:"%s"', { + cwd: gitDir, + encoding: 'utf8' + }).trim(); + + // Look for merge commit patterns from develop + const isMergeFromDevelop = lastCommitMessage.includes('Merge branch \'develop\'') || + lastCommitMessage.includes('Merge pull request') || + lastCommitMessage.includes('develop'); + + return isMergeFromDevelop; + } catch (error) { + return false; + } + + } catch (error) { + console.warn('[VersionUtils] Error checking for stable release trigger:', error.message); + return false; + } +} + +module.exports = { + getCurrentVersionInfo, + getCurrentVersion, + analyzeCommitsForVersionBump, + calculateNextVersion, + shouldTriggerStableRelease +}; \ No newline at end of file diff --git a/src/public/js/main.js b/src/public/js/main.js index 306d99a36..54fd13b15 100644 --- a/src/public/js/main.js +++ b/src/public/js/main.js @@ -181,9 +181,12 @@ document.addEventListener('DOMContentLoaded', function() { // Check if this is a release candidate const versionBadge = document.getElementById('version-badge'); if (versionBadge && data.version) { - const isRC = data.version.includes('-rc') || - data.version.includes('-alpha') || - data.version.includes('-beta'); + const isVersionRC = data.version.includes('-rc') || + data.version.includes('-alpha') || + data.version.includes('-beta'); + const isDevelopBranch = data.isDevelopment || data.gitBranch === 'develop'; + const isRC = isVersionRC || isDevelopBranch; + if (isRC) { versionBadge.textContent = 'RC'; versionBadge.classList.remove('hidden'); @@ -191,9 +194,11 @@ document.addEventListener('DOMContentLoaded', function() { } // Also update the page title - const isRC = data.version && (data.version.includes('-rc') || - data.version.includes('-alpha') || - data.version.includes('-beta')); + const isVersionRC = data.version && (data.version.includes('-rc') || + data.version.includes('-alpha') || + data.version.includes('-beta')); + const isDevelopBranch = data.isDevelopment || data.gitBranch === 'develop'; + const isRC = isVersionRC || isDevelopBranch; document.title = isRC ? 'Pulse RC' : 'Pulse'; // Check if update is available diff --git a/src/public/js/ui/settings.js b/src/public/js/ui/settings.js index 92dcece42..bbb907c9a 100644 --- a/src/public/js/ui/settings.js +++ b/src/public/js/ui/settings.js @@ -58,15 +58,15 @@ PulseApp.ui.settings = (() => { const tabButtons = document.querySelectorAll('.settings-tab'); tabButtons.forEach(button => { - button.addEventListener('click', (e) => { + button.addEventListener('click', async (e) => { e.preventDefault(); const tabName = e.currentTarget.getAttribute('data-tab'); - switchTab(tabName); + await switchTab(tabName); }); }); } - function switchTab(tabName) { + async function switchTab(tabName) { // Preserve current form data before switching tabs preserveCurrentFormData(); @@ -91,6 +91,11 @@ PulseApp.ui.settings = (() => { // Update content renderTabContent(); + // Load current version if system tab is active (after DOM element exists) + if (activeTab === 'system') { + await loadCurrentVersion(); + } + // Restore form data for the new tab restoreFormData(); } @@ -107,7 +112,7 @@ PulseApp.ui.settings = (() => { await loadConfiguration(); // Switch to requested tab - switchTab(tabName); + await switchTab(tabName); } function closeModal() { @@ -130,6 +135,24 @@ PulseApp.ui.settings = (() => { PulseApp.apiClient.handleError(error, 'Load configuration', showMessage); } } + + async function loadCurrentVersion() { + try { + const versionData = await PulseApp.apiClient.get('/api/version'); + const currentVersionElement = document.getElementById('current-version'); + if (currentVersionElement && versionData.version) { + currentVersionElement.textContent = versionData.version; + // Update currentConfig with the dynamic version for consistency + currentConfig.version = versionData.version; + } + } catch (error) { + console.warn('Could not load current version:', error); + const currentVersionElement = document.getElementById('current-version'); + if (currentVersionElement) { + currentVersionElement.textContent = currentConfig.version || 'Unknown'; + } + } + } function renderTabContent() { const container = document.getElementById('settings-modal-body'); @@ -580,7 +603,7 @@ PulseApp.ui.settings = (() => {

- Current Version: ${currentConfig.version || 'Unknown'} + Current Version: Loading...

Latest Version: Checking... diff --git a/temp-release/pulse-v3.24.0/Dockerfile b/temp-release/pulse-v3.24.0/Dockerfile deleted file mode 100644 index 36c2466ca..000000000 --- a/temp-release/pulse-v3.24.0/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -# ---- Builder Stage ---- -FROM node:18-alpine AS builder - -WORKDIR /usr/src/app - -# Install necessary build tools (if any, e.g., python, make for some native deps) -# RUN apk add --no-cache ... - -# Copy only necessary package files first -COPY package*.json ./ - -# Install ALL dependencies (including dev needed for build) -# Using npm ci for faster, more reliable builds in CI/CD -RUN npm ci - -# Copy the rest of the application code -# Important: Copy . before running build commands -COPY . . - -# Build the production CSS -RUN npm run build:css - -# Prune devDependencies after build -RUN npm prune --production - -# ---- Runner Stage ---- -FROM node:18-alpine - -WORKDIR /usr/src/app - -# Use existing node user (uid:gid 1000:1000) instead of system service accounts -# The node:18-alpine image already has a 'node' user with uid:gid 1000:1000 - -# Copy necessary files from builder stage -# Copy node_modules first (can be large) -COPY --from=builder /usr/src/app/node_modules ./node_modules -# Copy built assets -COPY --from=builder /usr/src/app/src/public ./src/public -# Copy server code -COPY --from=builder /usr/src/app/server ./server -# Copy root package.json needed for npm start and potentially other metadata -COPY --from=builder /usr/src/app/package.json ./ -# Optionally copy other root files if needed by the application (e.g., .env.example, README) -# COPY --from=builder /usr/src/app/.env.example ./ - -# Create config directory for persistent volume mount and data directory -RUN mkdir -p /usr/src/app/config /usr/src/app/data - -# Ensure correct ownership of application files -# Use /usr/src/app to cover everything copied -RUN chown -R node:node /usr/src/app - -# Switch to non-root user -USER node - -# Set environment variable to indicate Docker deployment -ENV DOCKER_DEPLOYMENT=true - -# Expose port -EXPOSE 7655 - -# Run the application using the start script -CMD [ "npm", "run", "start" ] \ No newline at end of file diff --git a/temp-release/pulse-v3.24.0/docker-compose.yml b/temp-release/pulse-v3.24.0/docker-compose.yml deleted file mode 100644 index 50f871cb4..000000000 --- a/temp-release/pulse-v3.24.0/docker-compose.yml +++ /dev/null @@ -1,35 +0,0 @@ -services: - pulse-server: - # Build context commented out - using pre-built image from Docker Hub - build: - context: . - dockerfile: Dockerfile - # image: rcourtman/pulse:latest # Use the pre-built image from Docker Hub - container_name: pulse - restart: unless-stopped - user: "1000:1000" # Run as standard user, not system service accounts - ports: - # Map container port 7655 to host port 7655 - # You can change the host port (left side) if 7655 is already in use on your host - - "7655:7655" - # env_file: - # NOTE: .env file is now managed by the web UI and stored in persistent volume - # No need to load from host .env file - # - .env - volumes: - # Persist configuration data to avoid losing settings on container recreation - # Mount a persistent volume for configuration files - - pulse_config:/usr/src/app/config - # Optional: Define networks if needed, otherwise uses default bridge network - # networks: - # - pulse_network - -# Define persistent volumes for configuration and data -volumes: - pulse_config: - driver: local - -# Optional: Define a network -# networks: -# pulse_network: -# driver: bridge \ No newline at end of file