37b56ba11c
* feat(automations): reliable cron scheduling and webhook-driven push/CI triggers Automations were "wounded": cron schedules mishandled timezones and some never fired, and the GitHub push/CI trigger types were coming-soon stubs. Cron (fix): - The scheduler reduced every cron expression to a fixed interval derived only from a */N step in the minute/hour field, so any pinned schedule (0 9 * * *, 30 14 * * *, weekday ranges, day-of-month) never fired, and the configured timezone was parsed but never used. - Replace the interval approximation with robfig/cron/v3: shouldFire now computes the true next fire time in the configured timezone (DST-aware, defaulting to UTC for host-independence), anchored to last-evaluated or creation time. Dead interval-parsing helpers and shorthand constants removed. GitHub push + CI triggers (feat): - Implement push and CI-failure triggers end to end via the existing per-workspace GitHub App webhook plumbing (#1810): the HMAC-verified webhook service now parses push and completed check_run events, resolves the installation to its workspaces, and publishes normalized bus events. - A new automation subscriber matches enabled github_push / github_ci triggers by workspace + repo + branch/conclusion/check-name filters and fires them (deduped by commit SHA / check-run id). The old poll-based stub evaluators are removed; the PR poll path is unchanged. - Subscribe the App manifest to push/check_run and enable both trigger types in the registry (CI defaults to the failure conclusion). Docs: - Update docs/public/feature-status.md and automation-and-mcp.md so the cron and push/CI rows reflect shipped reality. Tests: regression tests pin the previously-never-firing cron shapes, timezone/DST behavior, and host-independence; backend tests cover the webhook parse->publish and subscriber match/fire/dedup flows for both push and CI-failure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(automations): populate push.message and ci.url placeholders in fired trigger data Address PR review blockers: the default push and CI automation prompts reference {{push.message}} and {{ci.url}}, which the interpolator maps to data["message"] / data["html_url"] — but the webhook subscriber never populated those fields, so both always rendered empty. - Parse head_commit.message (push) and check_run.html_url (CI) in the GitHub webhook service and carry them on the published bus payloads. - Include "message" / "html_url" in the fired trigger data so the placeholders resolve; extend the webhook and subscriber tests to assert both flow through end to end. Non-blocking review cleanups: - Replace the misleading reuse of matchesAuthors for CI conclusions/check names with a clearly-named matchesFilterValue helper. - Drop dead context.Background() line and its unused import from the cron regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(automations): scope webhook triggers to active installs and per-branch dedup Address CodeRabbit review on the push/CI webhook triggers: - Security: resolveWorkspaceIDs now only returns connections whose status is active, so a suspended or revoked GitHub App installation can no longer fire push/CI automations. - Correctness: include the branch in the push dedup key (push:owner/name@branch@sha) so the same commit pushed to two matching branches fires once per branch instead of being suppressed. - Correctness: add an optional head-branch filter to github_ci triggers and gate CI matching on it, matching the push trigger's branch-scoping. - Stability: make the webhook subscriber's Start retryable — a subscription failure rolls back partial subscriptions and leaves the subscriber un-started so a later Start re-attempts, instead of permanently disabling push/CI processing. - Tests: cover multi-connection workspace resolution (dedup + inactive filtering), per-branch push dedup, and CI branch filtering. - Docs: note the CI branch filter and per-branch push dedup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(automations): cover webhook subscriber Start retry-after-failure path Add TestGitHubWebhookSubscriberStartRetryableAfterFailure with a minimal flaky bus whose second Subscribe fails, pinning the retry contract: a subscription failure rolls back the partial subscription and leaves the subscriber un-started, and a later Start re-attempts and succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71 lines
2.2 KiB
Go
71 lines
2.2 KiB
Go
package github
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func TestHTTPGitHubWebhookRoutesByAppRegistration(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
store := &githubWebhookMemoryStore{}
|
|
webhooks := NewAppRegistrationWebhookService(
|
|
"registration-work", "work-secret", store, nil, nil, nil,
|
|
)
|
|
runtime := &githubAppRuntime{
|
|
registrationID: "registration-work", source: DeploymentAppSourceManaged,
|
|
generation: 1, webhookAuth: webhooks,
|
|
}
|
|
service := &Service{appRegistrationRuntimes: map[string]*githubAppRuntime{
|
|
"registration-work": runtime,
|
|
}}
|
|
router := gin.New()
|
|
NewController(service, nil).RegisterHTTPRoutes(router)
|
|
|
|
payload := `{"action":"created","installation":{"id":42}}`
|
|
request := signedWebhookRequest(
|
|
"work-secret", "delivery-1", "installation", []byte(payload),
|
|
)
|
|
response := performWebhookRequest(
|
|
router, "/api/v1/github/app/registrations/registration-work/webhook", request,
|
|
)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("matching route status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
if len(store.deliveries) != 1 {
|
|
t.Fatalf("matching route deliveries = %#v", store.deliveries)
|
|
}
|
|
|
|
response = performWebhookRequest(
|
|
router, "/api/v1/github/app/registrations/registration-personal/webhook", request,
|
|
)
|
|
if response.Code != http.StatusConflict || len(store.deliveries) != 1 {
|
|
t.Fatalf(
|
|
"wrong route status = %d, deliveries = %#v, body = %s",
|
|
response.Code, store.deliveries, response.Body.String(),
|
|
)
|
|
}
|
|
|
|
response = performWebhookRequest(router, "/api/v1/github/app/webhook", request)
|
|
if response.Code != http.StatusNotFound {
|
|
t.Fatalf("legacy route status = %d, want 404", response.Code)
|
|
}
|
|
}
|
|
|
|
func performWebhookRequest(
|
|
router http.Handler,
|
|
path string,
|
|
request GitHubWebhookRequest,
|
|
) *httptest.ResponseRecorder {
|
|
httpRequest := httptest.NewRequest(http.MethodPost, path, strings.NewReader(string(request.Payload)))
|
|
httpRequest.Header.Set("X-GitHub-Delivery", request.DeliveryID)
|
|
httpRequest.Header.Set("X-GitHub-Event", request.Event)
|
|
httpRequest.Header.Set("X-Hub-Signature-256", request.Signature)
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, httpRequest)
|
|
return response
|
|
}
|