Skip to main content

Overview

The rollback endpoint allows administrators to close all pull requests created during a burst attack window. This feature is designed to clean up spam PRs created by bots or coordinated abuse campaigns.
This endpoint is protected by admin authentication and strict rate limiting (5 requests/minute).

Endpoint

POST /admin/rollback

Authentication

The endpoint accepts two forms of authentication:
  1. Static password - The PANIC_PASSWORD environment variable configured during deployment
  2. Single-use action token - Time-limited tokens (10 minutes TTL) generated for ntfy alert action buttons

Request Body

password
string
Admin password for rollback control. Must match the PANIC_PASSWORD environment variable.
token
string
Single-use action token generated by the system. Expires after 10 minutes.
You must provide either password or token, but not both.

Response

closed
integer
Number of PRs successfully closed.
failed
integer
Number of PRs that failed to close.
closed_urls
array
Array of PR URLs that were successfully closed.
failed_urls
array
Array of PR URLs that failed to close (with error details logged server-side).

Behavior

2-Hour Window

The rollback mechanism tracks PRs created during burst activity. PRs are registered for rollback only when:
  1. A global burst alert is active (triggered when suspicious activity is detected)
  2. The PR is less than 2 hours old (older entries are automatically pruned)
This ensures rollback only affects recent burst activity, not legitimate PRs.

Concurrent Processing

PRs are closed in parallel using up to 5 concurrent workers to minimize API rate limits and processing time.

Rate Limiting

The rollback endpoint has its own rate limit:
  • 5 requests per minute (regardless of IP)
  • Exceeding this limit returns 429 Too Many Requests

Examples

curl -X POST https://gitgost.leapcell.app/admin/rollback \
  -H "Content-Type: application/json" \
  -d '{"password":"<PANIC_PASSWORD>"}'

How PRs Are Tracked

From handlers.go:332-349, PRs are registered during push operations:
// Register PR URL for potential burst rollback only while a burst alert is active;
// prune entries older than TTL regardless.
if isGlobalBurstAlertActive() {
	nowPR := time.Now()
	recentBurstPRsMu.Lock()
	cutoffPR := nowPR.Add(-recentBurstPRsTTL)
	newURLs := recentBurstPRs[:0]
	newAts := recentBurstPRsAt[:0]
	for i, at := range recentBurstPRsAt {
		if at.After(cutoffPR) {
			newURLs = append(newURLs, recentBurstPRs[i])
			newAts = append(newAts, at)
		}
	}
	newURLs = append(newURLs, prURL)
	newAts = append(newAts, nowPR)
	recentBurstPRs = newURLs
	recentBurstPRsAt = newAts
	recentBurstPRsMu.Unlock()
}

Implementation Details

From handlers.go:826-909:
func RollbackBurstHandler(c *gin.Context) {
	var req struct {
		Password string `json:"password"`
		Token    string `json:"token"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
		return
	}

	// Accept either the static password or a valid single-use action token
	authorized := (panicPassword != "" && req.Password == panicPassword) ||
		(req.Token != "" && consumeActionToken(req.Token))
	if !authorized {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
		return
	}

	// Rate limit: max rollbackLimitMax calls per rollbackLimitWin
	now := time.Now()
	rollbackLimitMu.Lock()
	valid := rollbackLimitTimes[:0]
	for _, t := range rollbackLimitTimes {
		if now.Sub(t) < rollbackLimitWin {
			valid = append(valid, t)
		}
	}
	valid = append(valid, now)
	rollbackLimitTimes = valid
	exceeded := len(valid) > rollbackLimitMax
	rollbackLimitMu.Unlock()
	if exceeded {
		c.JSON(http.StatusTooManyRequests, gin.H{"error": "rollback rate limit exceeded"})
		return
	}

	recentBurstPRsMu.Lock()
	toClose := make([]string, len(recentBurstPRs))
	copy(toClose, recentBurstPRs)
	recentBurstPRs = recentBurstPRs[:0]
	recentBurstPRsAt = recentBurstPRsAt[:0]
	recentBurstPRsMu.Unlock()

	if len(toClose) == 0 {
		c.JSON(http.StatusOK, gin.H{"closed": 0, "message": "no burst PRs to close"})
		return
	}

	const maxCloseWorkers = 5
	closeConcurrency := make(chan struct{}, maxCloseWorkers)
	var (
		wg     sync.WaitGroup
		mu     sync.Mutex
		closed []string
		failed []string
	)
	for _, prURL := range toClose {
		wg.Add(1)
		go func(u string) {
			defer wg.Done()
			closeConcurrency <- struct{}{}
			defer func() { <-closeConcurrency }()
			if err := github.ClosePRByURL(u); err != nil {
				utils.Log("rollback: failed to close %s: %v", u, err)
				mu.Lock()
				failed = append(failed, u)
				mu.Unlock()
			} else {
				mu.Lock()
				closed = append(closed, u)
				mu.Unlock()
			}
		}(prURL)
	}
	wg.Wait()

	utils.Log("rollback: closed %d PRs, failed %d", len(closed), len(failed))
	c.JSON(http.StatusOK, gin.H{
		"closed":      len(closed),
		"failed":      len(failed),
		"closed_urls": closed,
		"failed_urls": failed,
	})
}

When to Use Rollback

Use the rollback endpoint when:
  • You’ve received a burst alert notification from ntfy
  • You’ve identified a pattern of spam PRs in the recent activity logs
  • You’ve activated panic mode and want to clean up the damage
  • You need to close multiple abusive PRs quickly without manual GitHub UI interaction
Rollback only affects PRs created during the active burst window (up to 2 hours). Legitimate PRs created before the attack are unaffected.

Build docs developers (and LLMs) love