Add GitHub commit polling for new commit detection

Implements local polling-based commit detection without requiring webhooks or public endpoints. Perfect for local development workflow.

Backend Changes:
- Added commit tracking columns to projects table:
  * latest_commit_sha: SHA of latest commit on branch
  * latest_commit_message: Commit message preview
  * latest_commit_author: Commit author name
  * latest_commit_date: Commit timestamp
  * last_commit_check: Last time commits were checked
- Created commitPoller service that:
  * Polls all projects every 60 seconds (configurable)
  * Uses GitHub API to check for new commits on tracked branch
  * Compares latest remote commit with deployed commit
  * Updates project with latest commit information
- Integrated poller into server lifecycle (start/stop)

Frontend Changes:
- Enhanced pipeline to detect new commits:
  * Compares project.latest_commit_sha with deployment.commit_sha
  * Shows yellow warning in GitHub stage when new commit detected
  * Displays commit message preview (first 50 chars)
  * Action button: "Deploy new?" for one-click deployment
- New commit detection shown when deployment is running or stopped
- Does not interfere with active builds/deployments

Benefits:
- Works entirely locally without exposing ports
- No webhook configuration required
- Uses existing GitHub access tokens
- Real-time commit awareness within 60 seconds
- Helps catch commits before pushing to production
- Perfect for intermediate testing step before Railway deployment
This commit is contained in:
2025-11-13 07:06:27 +00:00
parent 9d3b6f4bfc
commit 3da3504152
4 changed files with 125 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ const analyticsCollector = require('./services/analyticsCollector');
const statusMonitor = require('./services/statusMonitor');
const healthMonitor = require('./services/healthMonitor');
const logger = require('./services/logger');
const commitPoller = require('./services/commitPoller');
const app = express();
const server = http.createServer(app);
@@ -98,6 +99,7 @@ setupWebSocketServer(server);
analyticsCollector.startStatsCollection();
statusMonitor.start();
healthMonitor.start();
commitPoller.start(60000);
logger.info('miniPaaS Control Plane initializing...');
@@ -113,6 +115,7 @@ process.on('SIGTERM', () => {
logger.info('Shutting down gracefully...');
analyticsCollector.stopStatsCollection();
healthMonitor.stop();
commitPoller.stop();
server.close(() => {
logger.info('Server closed');
process.exit(0);

View File

@@ -0,0 +1,102 @@
const db = require('../config/database');
const { decrypt } = require('../utils/encryption');
const { getLatestCommit } = require('./githubService');
class CommitPoller {
constructor() {
this.pollingInterval = null;
this.isPolling = false;
}
start(intervalMs = 60000) {
if (this.isPolling) {
console.log('[Commit Poller] Already running');
return;
}
console.log(`[Commit Poller] Starting with ${intervalMs}ms interval`);
this.isPolling = true;
this.checkAllProjects();
this.pollingInterval = setInterval(() => {
this.checkAllProjects();
}, intervalMs);
}
stop() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = null;
this.isPolling = false;
console.log('[Commit Poller] Stopped');
}
}
async checkAllProjects() {
try {
const projects = await db.query(
`SELECT id, github_repo_name, github_branch, github_access_token, latest_commit_sha
FROM projects
WHERE github_repo_name IS NOT NULL`
);
for (const project of projects.rows) {
try {
await this.checkProjectCommits(project);
} catch (error) {
console.error(`[Commit Poller] Error checking project ${project.id}:`, error.message);
}
}
} catch (error) {
console.error('[Commit Poller] Error fetching projects:', error);
}
}
async checkProjectCommits(project) {
try {
const accessToken = decrypt(project.github_access_token);
const latestCommit = await getLatestCommit(
project.github_repo_name,
project.github_branch,
accessToken
);
const hasNewCommit = project.latest_commit_sha &&
project.latest_commit_sha !== latestCommit.sha;
await db.query(
`UPDATE projects
SET latest_commit_sha = $1,
latest_commit_message = $2,
latest_commit_author = $3,
latest_commit_date = $4,
last_commit_check = NOW()
WHERE id = $5`,
[
latestCommit.sha,
latestCommit.message,
latestCommit.author,
latestCommit.date,
project.id
]
);
if (hasNewCommit) {
console.log(`[Commit Poller] New commit detected for project ${project.id}: ${latestCommit.sha.substring(0, 7)}`);
}
return {
hasNewCommit,
latestCommit
};
} catch (error) {
console.error(`[Commit Poller] Failed to check commits for project ${project.id}:`, error.message);
throw error;
}
}
}
const commitPoller = new CommitPoller();
module.exports = commitPoller;

View File

@@ -94,6 +94,11 @@ function getPipelineStatus(project) {
deploy: { status: 'idle', message: 'Not deployed', action: null }
};
const hasNewCommit = project.latest_commit_sha &&
latestDeployment &&
latestDeployment.commit_sha &&
project.latest_commit_sha !== latestDeployment.commit_sha;
if (!latestDeployment) {
pipeline.github.status = 'warning';
pipeline.github.message = 'No deployments yet';
@@ -143,6 +148,16 @@ function getPipelineStatus(project) {
pipeline.deploy.action = { text: 'Start?', fn: `deployProject(${project.id})` };
}
if (hasNewCommit && (status === 'running' || status === 'stopped')) {
pipeline.github.status = 'warning';
pipeline.github.message = 'New commit detected';
if (project.latest_commit_message) {
const shortMessage = project.latest_commit_message.split('\n')[0].substring(0, 50);
pipeline.github.error = shortMessage;
}
pipeline.github.action = { text: 'Deploy new?', fn: `deployProject(${project.id})` };
}
return pipeline;
}

View File

@@ -28,6 +28,11 @@ CREATE TABLE IF NOT EXISTS projects (
build_cache_enabled BOOLEAN DEFAULT true,
webhook_token VARCHAR(255),
webhook_enabled BOOLEAN DEFAULT false,
latest_commit_sha VARCHAR(40),
latest_commit_message TEXT,
latest_commit_author VARCHAR(255),
latest_commit_date TIMESTAMP,
last_commit_check TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);