Skip to content

Commit ebc4964

Browse files
committed
feat: refactor for maintainability
1 parent a1ddd86 commit ebc4964

File tree

3 files changed

+329
-312
lines changed

3 files changed

+329
-312
lines changed

internal/app/app.go

Lines changed: 4 additions & 312 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Package app provides the core application logic for the GitHub bot.
2-
// coordinates webhook processing, Okta sync, and PR compliance checks.
2+
// Coordinates webhook processing, Okta sync, and PR compliance checks.
33
package app
44

55
import (
@@ -11,10 +11,8 @@ import (
1111
"github.com/cruxstack/github-ops-app/internal/config"
1212
internalerrors "github.com/cruxstack/github-ops-app/internal/errors"
1313
"github.com/cruxstack/github-ops-app/internal/github/client"
14-
"github.com/cruxstack/github-ops-app/internal/github/webhooks"
1514
"github.com/cruxstack/github-ops-app/internal/notifiers"
1615
"github.com/cruxstack/github-ops-app/internal/okta"
17-
gh "github.com/google/go-github/v79/github"
1816
)
1917

2018
// App is the main application instance containing all clients and
@@ -28,7 +26,7 @@ type App struct {
2826
}
2927

3028
// New creates a new App instance with configured clients.
31-
// initializes GitHub, Okta, and Slack clients based on config.
29+
// Initializes GitHub, Okta, and Slack clients based on config.
3230
func New(ctx context.Context, cfg *config.Config) (*App, error) {
3331
logger := config.NewLogger()
3432

@@ -87,7 +85,7 @@ type ScheduledEvent struct {
8785
}
8886

8987
// ProcessScheduledEvent handles scheduled events (e.g., cron jobs).
90-
// routes to appropriate handlers based on event action.
88+
// Routes to appropriate handlers based on event action.
9189
func (a *App) ProcessScheduledEvent(ctx context.Context, evt ScheduledEvent) error {
9290
if a.Config.DebugEnabled {
9391
j, _ := json.Marshal(evt)
@@ -105,7 +103,7 @@ func (a *App) ProcessScheduledEvent(ctx context.Context, evt ScheduledEvent) err
105103
}
106104

107105
// ProcessWebhook handles incoming GitHub webhook events.
108-
// supports pull_request, team, and membership events.
106+
// Supports pull_request, team, and membership events.
109107
func (a *App) ProcessWebhook(ctx context.Context, payload []byte, eventType string) error {
110108
if a.Config.DebugEnabled {
111109
a.Logger.Debug("received webhook", slog.String("event_type", eventType))
@@ -123,312 +121,6 @@ func (a *App) ProcessWebhook(ctx context.Context, payload []byte, eventType stri
123121
}
124122
}
125123

126-
// handleOktaSync executes Okta group synchronization to GitHub teams.
127-
// sends Slack notification with sync results if configured.
128-
func (a *App) handleOktaSync(ctx context.Context) error {
129-
if !a.Config.IsOktaSyncEnabled() {
130-
a.Logger.Info("okta sync is not enabled, skipping")
131-
return nil
132-
}
133-
134-
if a.OktaClient == nil || a.GitHubClient == nil {
135-
return errors.Wrap(internalerrors.ErrClientNotInit, "okta or github client")
136-
}
137-
138-
syncer := okta.NewSyncer(a.OktaClient, a.GitHubClient, a.Config.OktaSyncRules, a.Config.OktaSyncSafetyThreshold, a.Logger)
139-
syncResult, err := syncer.Sync(ctx)
140-
if err != nil {
141-
return errors.Wrap(err, "okta sync failed")
142-
}
143-
144-
a.Logger.Info("okta sync completed", slog.Int("report_count", len(syncResult.Reports)))
145-
146-
if a.Notifier != nil {
147-
if err := a.Notifier.NotifyOktaSync(ctx, syncResult.Reports, a.Config.GitHubOrg); err != nil {
148-
a.Logger.Warn("failed to send slack notification", slog.String("error", err.Error()))
149-
}
150-
}
151-
152-
if a.Config.OktaOrphanedUserNotifications {
153-
syncedTeams := make([]string, 0, len(syncResult.Reports))
154-
for _, report := range syncResult.Reports {
155-
syncedTeams = append(syncedTeams, report.GitHubTeam)
156-
}
157-
158-
orphanedReport, err := syncer.DetectOrphanedUsers(ctx, syncedTeams)
159-
if err != nil {
160-
a.Logger.Warn("failed to detect orphaned users", slog.String("error", err.Error()))
161-
} else if orphanedReport != nil && len(orphanedReport.OrphanedUsers) > 0 {
162-
a.Logger.Info("orphaned users detected", slog.Int("count", len(orphanedReport.OrphanedUsers)))
163-
164-
if a.Notifier != nil {
165-
if err := a.Notifier.NotifyOrphanedUsers(ctx, orphanedReport); err != nil {
166-
a.Logger.Warn("failed to send orphaned users notification", slog.String("error", err.Error()))
167-
}
168-
}
169-
}
170-
}
171-
172-
return nil
173-
}
174-
175-
// handlePullRequestWebhook processes GitHub pull request webhook events.
176-
// checks merged PRs for branch protection compliance violations.
177-
func (a *App) handlePullRequestWebhook(ctx context.Context, payload []byte) error {
178-
prEvent, err := webhooks.ParsePullRequestEvent(payload)
179-
if err != nil {
180-
return err
181-
}
182-
183-
if !prEvent.IsMerged() {
184-
if a.Config.DebugEnabled {
185-
a.Logger.Debug("pr not merged, skipping", slog.Int("pr_number", prEvent.Number))
186-
}
187-
return nil
188-
}
189-
190-
baseBranch := prEvent.GetBaseBranch()
191-
if !a.Config.ShouldMonitorBranch(baseBranch) {
192-
if a.Config.DebugEnabled {
193-
a.Logger.Debug("branch not monitored, skipping", slog.String("branch", baseBranch))
194-
}
195-
return nil
196-
}
197-
198-
ghClient := a.GitHubClient
199-
200-
if prEvent.GetInstallationID() != 0 && prEvent.GetInstallationID() != a.Config.GitHubInstallationID {
201-
installClient, err := client.NewAppClientWithBaseURL(
202-
a.Config.GitHubAppID,
203-
prEvent.GetInstallationID(),
204-
a.Config.GitHubAppPrivateKey,
205-
a.Config.GitHubOrg,
206-
a.Config.GitHubBaseURL,
207-
)
208-
if err != nil {
209-
return errors.Wrapf(err, "failed to create client for installation %d", prEvent.GetInstallationID())
210-
}
211-
ghClient = installClient
212-
}
213-
214-
if ghClient == nil {
215-
return errors.Wrap(internalerrors.ErrClientNotInit, "github client")
216-
}
217-
218-
owner := prEvent.GetRepoOwner()
219-
repo := prEvent.GetRepoName()
220-
221-
result, err := ghClient.CheckPRCompliance(ctx, owner, repo, prEvent.Number)
222-
if err != nil {
223-
return errors.Wrapf(err, "failed to check pr #%d compliance", prEvent.Number)
224-
}
225-
226-
if result.WasBypassed() {
227-
a.Logger.Info("pr bypassed branch protection",
228-
slog.Int("pr_number", prEvent.Number),
229-
slog.String("branch", baseBranch))
230-
231-
if a.Notifier != nil {
232-
repoFullName := prEvent.GetRepoFullName()
233-
if err := a.Notifier.NotifyPRBypass(ctx, result, repoFullName); err != nil {
234-
a.Logger.Warn("failed to send slack notification", slog.String("error", err.Error()))
235-
}
236-
}
237-
} else if a.Config.DebugEnabled {
238-
a.Logger.Debug("pr complied with branch protection", slog.Int("pr_number", prEvent.Number))
239-
}
240-
241-
return nil
242-
}
243-
244-
// handleTeamWebhook processes GitHub team webhook events.
245-
// triggers Okta sync when team changes are made externally.
246-
func (a *App) handleTeamWebhook(ctx context.Context, payload []byte) error {
247-
teamEvent, err := webhooks.ParseTeamEvent(payload)
248-
if err != nil {
249-
return err
250-
}
251-
252-
if !a.Config.IsOktaSyncEnabled() {
253-
if a.Config.DebugEnabled {
254-
a.Logger.Debug("okta sync not enabled, skipping team webhook")
255-
}
256-
return nil
257-
}
258-
259-
if a.shouldIgnoreWebhookChange(ctx, teamEvent) {
260-
if a.Config.DebugEnabled {
261-
a.Logger.Debug("ignoring team change from bot/app",
262-
slog.String("action", teamEvent.Action),
263-
slog.String("sender", teamEvent.GetSenderLogin()))
264-
}
265-
return nil
266-
}
267-
268-
a.Logger.Info("external team change detected, triggering sync",
269-
slog.String("action", teamEvent.Action),
270-
slog.String("team", teamEvent.GetTeamSlug()),
271-
slog.String("sender", teamEvent.GetSenderLogin()))
272-
273-
return a.handleOktaSync(ctx)
274-
}
275-
276-
// handleMembershipWebhook processes GitHub membership webhook events.
277-
// triggers Okta sync when team memberships are changed externally.
278-
func (a *App) handleMembershipWebhook(ctx context.Context, payload []byte) error {
279-
membershipEvent, err := webhooks.ParseMembershipEvent(payload)
280-
if err != nil {
281-
return err
282-
}
283-
284-
if !membershipEvent.IsTeamScope() {
285-
if a.Config.DebugEnabled {
286-
a.Logger.Debug("membership event is not team scope, skipping")
287-
}
288-
return nil
289-
}
290-
291-
if !a.Config.IsOktaSyncEnabled() {
292-
if a.Config.DebugEnabled {
293-
a.Logger.Debug("okta sync not enabled, skipping membership webhook")
294-
}
295-
return nil
296-
}
297-
298-
if a.shouldIgnoreWebhookChange(ctx, membershipEvent) {
299-
if a.Config.DebugEnabled {
300-
a.Logger.Debug("ignoring membership change from bot/app",
301-
slog.String("action", membershipEvent.Action),
302-
slog.String("team", membershipEvent.GetTeamSlug()),
303-
slog.String("sender", membershipEvent.GetSenderLogin()))
304-
}
305-
return nil
306-
}
307-
308-
a.Logger.Info("external membership change detected, triggering sync",
309-
slog.String("action", membershipEvent.Action),
310-
slog.String("team", membershipEvent.GetTeamSlug()),
311-
slog.String("sender", membershipEvent.GetSenderLogin()))
312-
313-
return a.handleOktaSync(ctx)
314-
}
315-
316-
// webhookSender provides sender information for webhook events.
317-
type webhookSender interface {
318-
GetSenderType() string
319-
GetSenderLogin() string
320-
}
321-
322-
// shouldIgnoreWebhookChange checks if a webhook should be ignored.
323-
// ignores changes made by bots or the GitHub App itself to prevent loops.
324-
func (a *App) shouldIgnoreWebhookChange(ctx context.Context, event webhookSender) bool {
325-
if event.GetSenderType() == "Bot" {
326-
return true
327-
}
328-
329-
if a.GitHubClient != nil {
330-
appSlug, err := a.GitHubClient.GetAppSlug(ctx)
331-
if err != nil {
332-
a.Logger.Warn("failed to get app slug", slog.String("error", err.Error()))
333-
return false
334-
}
335-
if event.GetSenderLogin() == appSlug+"[bot]" {
336-
return true
337-
}
338-
}
339-
340-
return false
341-
}
342-
343-
// handleSlackTest sends test notifications to Slack with sample data.
344-
// useful for verifying Slack connectivity and previewing message formats.
345-
func (a *App) handleSlackTest(ctx context.Context) error {
346-
if a.Notifier == nil {
347-
return errors.New("slack is not configured")
348-
}
349-
350-
// test 1: PR bypass notification
351-
if err := a.Notifier.NotifyPRBypass(ctx, fakePRComplianceResult(), "acme-corp/demo-repo"); err != nil {
352-
return errors.Wrap(err, "failed to send test pr bypass notification")
353-
}
354-
a.Logger.Info("sent test pr bypass notification")
355-
356-
// test 2: Okta sync notification
357-
if err := a.Notifier.NotifyOktaSync(ctx, fakeOktaSyncReports(), "acme-corp"); err != nil {
358-
return errors.Wrap(err, "failed to send test okta sync notification")
359-
}
360-
a.Logger.Info("sent test okta sync notification")
361-
362-
// test 3: Orphaned users notification
363-
if err := a.Notifier.NotifyOrphanedUsers(ctx, fakeOrphanedUsersReport()); err != nil {
364-
return errors.Wrap(err, "failed to send test orphaned users notification")
365-
}
366-
a.Logger.Info("sent test orphaned users notification")
367-
368-
return nil
369-
}
370-
371-
// fakePRComplianceResult returns sample PR compliance data for testing.
372-
func fakePRComplianceResult() *client.PRComplianceResult {
373-
prNumber := 42
374-
prTitle := "Add new authentication feature"
375-
prURL := "https://github.com/acme-corp/demo-repo/pull/42"
376-
mergedByLogin := "test-user"
377-
378-
return &client.PRComplianceResult{
379-
PR: &gh.PullRequest{
380-
Number: &prNumber,
381-
Title: &prTitle,
382-
HTMLURL: &prURL,
383-
MergedBy: &gh.User{
384-
Login: &mergedByLogin,
385-
},
386-
},
387-
BaseBranch: "main",
388-
UserHasBypass: true,
389-
UserBypassReason: "repository admin",
390-
Violations: []client.ComplianceViolation{
391-
{Type: "insufficient_reviews", Description: "required 2 approving reviews, had 0"},
392-
{Type: "missing_status_check", Description: "required check 'ci/build' did not pass"},
393-
},
394-
}
395-
}
396-
397-
// fakeOktaSyncReports returns sample Okta sync reports for testing.
398-
func fakeOktaSyncReports() []*okta.SyncReport {
399-
return []*okta.SyncReport{
400-
{
401-
Rule: "engineering-team",
402-
OktaGroup: "Engineering",
403-
GitHubTeam: "engineering",
404-
MembersAdded: []string{"alice", "bob"},
405-
MembersRemoved: []string{"charlie"},
406-
},
407-
{
408-
Rule: "platform-team",
409-
OktaGroup: "Platform",
410-
GitHubTeam: "platform",
411-
// no changes
412-
},
413-
{
414-
Rule: "security-team",
415-
OktaGroup: "Security",
416-
GitHubTeam: "security",
417-
MembersAdded: []string{"dave"},
418-
MembersSkippedExternal: []string{"external-contractor"},
419-
MembersSkippedNoGHUsername: []string{"new-hire@example.com"},
420-
Errors: []string{"failed to fetch group members: rate limited"},
421-
},
422-
}
423-
}
424-
425-
// fakeOrphanedUsersReport returns sample orphaned users data for testing.
426-
func fakeOrphanedUsersReport() *okta.OrphanedUsersReport {
427-
return &okta.OrphanedUsersReport{
428-
OrphanedUsers: []string{"orphan-user-1", "orphan-user-2", "legacy-bot"},
429-
}
430-
}
431-
432124
// StatusResponse contains application status and feature flags.
433125
type StatusResponse struct {
434126
Status string `json:"status"`

0 commit comments

Comments
 (0)