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:
@@ -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);
|
||||
|
||||
102
control-plane/services/commitPoller.js
Normal file
102
control-plane/services/commitPoller.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user