Merge pull request #130 from rcourtman/develop

Release v3.25.0 - Automated Stable Release System
This commit is contained in:
rcourtman
2025-06-13 16:48:45 +01:00
committed by GitHub
17 changed files with 1542 additions and 141 deletions
+69
View File
@@ -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
+142
View File
@@ -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
+306
View File
@@ -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
+49 -7
View File
@@ -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!
### 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!
+69
View File
@@ -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
+2 -2
View File
@@ -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
+35 -1
View File
@@ -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.
+419
View File
@@ -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 "<type>: <description>"
```
**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
```
+1 -1
View File
@@ -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": {
+11
View File
@@ -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
+11 -3
View File
@@ -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, {
+76 -18
View File
@@ -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}`);
}
}
+313
View File
@@ -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
};
+11 -6
View File
@@ -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
+28 -5
View File
@@ -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 = (() => {
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-700 dark:text-gray-300">
Current Version: <span id="current-version" class="font-mono font-semibold">${currentConfig.version || 'Unknown'}</span>
Current Version: <span id="current-version" class="font-mono font-semibold">Loading...</span>
</p>
<p class="text-sm text-gray-700 dark:text-gray-300 mt-1">
<span id="latest-version-label">Latest Version</span>: <span id="latest-version" class="font-mono font-semibold text-gray-500 dark:text-gray-400">Checking...</span>
-63
View File
@@ -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" ]
@@ -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