--- phase: 08-dork-engine plan: 05 type: execute wave: 2 depends_on: [08-01] files_modified: - pkg/dorks/github.go - pkg/dorks/github_test.go autonomous: true requirements: - DORK-02 must_haves: truths: - "GitHubExecutor.Source() returns \"github\"" - "GitHubExecutor.Execute runs GitHub Code Search against api.github.com and returns []Match" - "Missing token returns ErrMissingAuth with setup instructions" - "Retry-After header is honored (sleep + retry once) for 403/429" - "Response items mapped to Match with URL, Path, Snippet (text_matches)" artifacts: - path: "pkg/dorks/github.go" provides: "GitHubExecutor implementing Executor interface" contains: "type GitHubExecutor struct" - path: "pkg/dorks/github_test.go" provides: "httptest server exercising success/auth/rate-limit paths" contains: "httptest.NewServer" key_links: - from: "pkg/dorks/github.go" to: "https://api.github.com/search/code" via: "net/http client" pattern: "api.github.com/search/code" - from: "pkg/dorks/github.go" to: "pkg/dorks/executor.go Executor interface" via: "interface satisfaction" pattern: "Execute\\(ctx" --- Implement the live GitHub Code Search executor — the only source that actually runs in Phase 8 (all other executors stay stubbed with ErrSourceNotImplemented). Hits `GET https://api.github.com/search/code?q={query}`, authenticated via GITHUB_TOKEN env var / viper config. Honors rate-limit response codes. Maps response items to pkg/dorks.Match entries consumable by the engine pipeline in downstream phases. Purpose: Satisfies the "GitHub live" slice of DORK-02 and unblocks `keyhunter dorks run --source=github` in Plan 08-06. Output: Working pkg/dorks.GitHubExecutor + httptest-backed test suite. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/phases/08-dork-engine/08-CONTEXT.md @.planning/phases/08-dork-engine/08-01-PLAN.md @pkg/dorks/executor.go ```go type Executor interface { Source() string Execute(ctx context.Context, d Dork, limit int) ([]Match, error) } type Match struct { DorkID string Source string URL string Snippet string Path string } var ErrMissingAuth = errors.New("dork source requires auth credentials") ``` Task 1: GitHubExecutor with net/http + Retry-After handling pkg/dorks/github.go, pkg/dorks/github_test.go - Test: Execute with empty token returns ErrMissingAuth (wrapped) without hitting HTTP - Test: Execute with httptest server returning 200 + items parses response into []Match with URL/Path/Snippet - Test: limit=5 caps returned Match count at 5 even if API returns 10 - Test: 403 with X-RateLimit-Remaining=0 and Retry-After=1 sleeps and retries once, then succeeds - Test: 401 returns ErrMissingAuth (token rejected) - Test: 422 (invalid query) returns a descriptive error containing the status code - Test: Source() returns "github" Create pkg/dorks/github.go: ```go package dorks import ( "context" "encoding/json" "fmt" "io" "net/http" "strconv" "time" ) type GitHubExecutor struct { Token string BaseURL string // default "https://api.github.com", overridable for tests HTTPClient *http.Client MaxRetries int // default 1 } func NewGitHubExecutor(token string) *GitHubExecutor { return &GitHubExecutor{ Token: token, BaseURL: "https://api.github.com", HTTPClient: &http.Client{Timeout: 30 * time.Second}, MaxRetries: 1, } } func (g *GitHubExecutor) Source() string { return "github" } type ghSearchResponse struct { TotalCount int `json:"total_count"` Items []struct { Name string `json:"name"` Path string `json:"path"` HTMLURL string `json:"html_url"` Repository struct { FullName string `json:"full_name"` } `json:"repository"` TextMatches []struct { Fragment string `json:"fragment"` } `json:"text_matches"` } `json:"items"` } func (g *GitHubExecutor) Execute(ctx context.Context, d Dork, limit int) ([]Match, error) { if g.Token == "" { return nil, fmt.Errorf("%w: set GITHUB_TOKEN env var or `keyhunter config set dorks.github.token ` (needs public_repo scope)", ErrMissingAuth) } if limit <= 0 || limit > 100 { limit = 30 } url := fmt.Sprintf("%s/search/code?q=%s&per_page=%d", g.BaseURL, urlQueryEscape(d.Query), limit) var resp *http.Response for attempt := 0; attempt <= g.MaxRetries; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/vnd.github.v3.text-match+json") req.Header.Set("Authorization", "Bearer "+g.Token) req.Header.Set("User-Agent", "keyhunter-dork-engine") r, err := g.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("github search: %w", err) } if r.StatusCode == http.StatusOK { resp = r break } body, _ := io.ReadAll(r.Body) r.Body.Close() switch r.StatusCode { case http.StatusUnauthorized: return nil, fmt.Errorf("%w: github token rejected (401)", ErrMissingAuth) case http.StatusForbidden, http.StatusTooManyRequests: if attempt < g.MaxRetries { sleep := parseRetryAfter(r.Header.Get("Retry-After")) select { case <-time.After(sleep): continue case <-ctx.Done(): return nil, ctx.Err() } } return nil, fmt.Errorf("github rate limit: %d %s", r.StatusCode, string(body)) default: return nil, fmt.Errorf("github search failed: %d %s", r.StatusCode, string(body)) } } defer resp.Body.Close() var parsed ghSearchResponse if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { return nil, fmt.Errorf("decoding github response: %w", err) } out := make([]Match, 0, len(parsed.Items)) for _, it := range parsed.Items { snippet := "" if len(it.TextMatches) > 0 { snippet = it.TextMatches[0].Fragment } out = append(out, Match{ DorkID: d.ID, Source: "github", URL: it.HTMLURL, Path: it.Repository.FullName + "/" + it.Path, Snippet: snippet, }) if len(out) >= limit { break } } return out, nil } func parseRetryAfter(v string) time.Duration { if v == "" { return time.Second } if secs, err := strconv.Atoi(v); err == nil { return time.Duration(secs) * time.Second } return time.Second } func urlQueryEscape(s string) string { return (&url.URL{Path: s}).EscapedPath() // wrong — use url.QueryEscape } ``` Fix the helper: import "net/url" and use `url.QueryEscape(s)` — do NOT hand-roll. Create pkg/dorks/github_test.go using httptest.NewServer. Override executor.BaseURL to the test server URL. One subtest per behavior case. For Retry-After test: server returns 403 with Retry-After: 1 on first request, 200 with fake items on second. Do NOT register GitHubExecutor into a global Runner here — Plan 08-06 does the wiring inside cmd/dorks.go via NewGitHubExecutor(viper.GetString(...)). cd /home/salva/Documents/apikey && go test ./pkg/dorks/... -run GitHub -v All GitHub executor test cases pass; Execute honors token, rate limit, and limit cap; Match fields populated from real response shape. `go test ./pkg/dorks/...` passes including all new GitHub cases. - pkg/dorks.GitHubExecutor implements Executor interface - Live GitHub Code Search calls are testable via httptest (BaseURL override) - ErrMissingAuth surfaces with actionable setup instructions - Retry-After respected once before giving up After completion, create `.planning/phases/08-dork-engine/08-05-SUMMARY.md`