This document provides guidance for AI agents (like Claude, GPT, etc.) working on the fly-search project.
fly-search is a CLI tool written in Go that searches across Concourse CI build logs efficiently. It solves the problem of manually running fly watch for each build when searching for specific patterns across multiple builds, jobs, and pipelines.
- Search across all pipelines or specific pipelines/jobs
- Intelligent two-tier caching system (build logs + metadata)
- Parallel log fetching for performance
- Regex pattern matching with context lines
- Multiple output formats (table, JSON)
- Task tracking within build logs
-
CLI Flags (
main.golines ~78-145)- Uses Go's
flagpackage with both long and short forms - All flags defined in
init()function - Short flags:
-t,-p,-a,-s,-j,-c,-o,-C,-u,-l
- Uses Go's
-
Caching System (
main.golines ~63-605)- Build Log Cache: 24-hour TTL, stores actual log content
- Metadata Cache: 5-minute TTL, stores pipeline/job/build relationships
- Smart caching: auto-updates when higher count requested
- Cache location:
~/.fly-search/cache/
-
Search Engine (
main.golines ~485-650)- Parallel search with configurable concurrency
- Regex pattern matching
- Task name extraction from build plans
- Context lines support
-
Output Formatters (
main.golines ~750+)- Table format with ANSI colors
- JSON format for programmatic use
- Build URL generation
Problem: Users may request different build counts (e.g., -c 1 then -c 10). We need to cache intelligently without over-fetching or cache-missing.
Solution (lines ~240-350):
type CachedMetadata struct {
BuildCounts map[string]int // Tracks how many builds cached per job
// ... other fields
}When loading cache:
- If
cachedCount >= requestedCount: Use cache, slice to requested count - If
cachedCount < requestedCount: Fetch additional builds, update cache
Key Code Locations:
- Cache structure: lines ~71-76
- Smart update logic: lines ~317-343
- Cache save: lines ~581-605
Problem: Searching all pipelines is expensive (hundreds of API calls). Users shouldn't accidentally do this.
Solution (lines ~123-145):
- Require either
--pipelineOR--all-pipelines(not both, not neither) - Explicit opt-in prevents accidental heavy operations
- Clear error messages guide users
Problem: Long-running operations appear frozen without feedback.
Solution:
- Discovery phase:
"Discovering builds... pipeline X/Y (name)"(line ~282) - Search phase:
"Searching builds... X/Y"with carriage return (line ~509) - Uses
\rto overwrite line in terminals - Completion message when done (line ~526)
Build Log Cache: SHA256(target + buildID) (line ~454)
- Unique per build, never changes
- Long TTL (24 hours) - builds don't change
Metadata Cache: SHA256(target) (line ~533)
- Per target, independent of count
- Short TTL (5 minutes) - jobs/builds change frequently
- Stores
BuildCountsmap to track per-job cache depth
// Good: Provide context in errors
if err != nil {
return fmt.Errorf("failed to fetch builds for %s/%s: %w", pipeline, job, err)
}
// Good: Silent failures for non-critical operations (during discovery)
if err != nil {
// Silently skip - too verbose to print every error
continue
}
// Bad: Generic errors without context
if err != nil {
return err
}// Good: Messages to stderr, results to stdout
fmt.Fprintf(os.Stderr, "Searching builds...\n")
// Good: Overwriting progress with \r
fmt.Fprintf(os.Stderr, "\rSearching builds... %d/%d", completed, total)
// Bad: Progress to stdout (pollutes results)
fmt.Printf("Searching...\n")// Good: Long and short flags
target = flag.String("target", "", "Concourse target name")
flag.StringVar(target, "t", "", "Shorthand for --target")
// Good: Clear descriptions, no redundant "default: X" in text
buildCount = flag.Int("count", 1, "Number of recent builds to search per job")// Good: Check cache first, fallback to API
cached, err := loadFromCache(target, buildID, maxAge)
if cached != nil {
return cached.Data
}
// Fetch from API...
// Good: Save to cache after successful fetch
if !*noCache {
saveToCache(target, buildID, data)
}- Declare variable at top of
init()function - Add long form:
flag.String/Int/Bool(...) - Add short form:
flag.XxxVar(&variable, "x", default, "Shorthand...") - Update README.md flags table
- Add validation in
main()if needed (lines ~120-150)
Important: Always maintain backwards compatibility with existing cache files!
- Update
CachedMetadataorCachedBuildstruct - Handle missing fields gracefully (use nil checks)
- Update
loadMetadataCache()andsaveMetadataCache() - Consider cache version/migration if breaking changes
- Add to
outputFmtflag validation - Create new function
outputXXX(results []SearchResult) - Call in
main()based on*outputFmtvalue (lines ~282-286) - Update README.md with examples
After code changes, test:
-
Basic functionality:
./fly-search -t TARGET -p PIPELINE -s "pattern" -
Caching behavior:
# First run (builds cache) ./fly-search -t TARGET -p PIPELINE -s "pattern" -c 5 # Second run (uses cache) ./fly-search -t TARGET -p PIPELINE -s "pattern2" -c 5 # Third run (updates cache) ./fly-search -t TARGET -p PIPELINE -s "pattern" -c 10
-
All-pipelines search:
# Small count (faster) ./fly-search -t TARGET -a -s "pattern" -c 1
-
Error cases:
# Should error: no pipeline or all-pipelines ./fly-search -t TARGET -s "pattern" # Should error: both pipeline and all-pipelines ./fly-search -t TARGET -p PIPELINE -a -s "pattern"
-
Flag variations:
# Long flags ./fly-search --target TARGET --pipeline PIPELINE --search "pattern" # Short flags ./fly-search -t TARGET -p PIPELINE -s "pattern" # Mixed ./fly-search -t TARGET --pipeline PIPELINE -s "pattern"
Expected timings (22 pipelines, ~200 jobs):
- First
-asearch (count=1): ~60 seconds (building metadata cache) - Second
-asearch (count=1, cached): ~0.2 seconds - Upgrade cache (count=10 after count=1): ~60 seconds (fetching more builds)
- Cached with higher count: ~instant (slicing cached builds)
Wrong:
cacheKey := fmt.Sprintf("%s/%s/%d", pipeline, job, buildCount)Right:
cacheKey := fmt.Sprintf("%s/%s", pipeline, job)
// Track count separately in BuildCounts mapWhy: Including count causes cache misses when users change count value.
Wrong:
fmt.Printf("Searching builds...\n") // Pollutes stdoutRight:
fmt.Fprintf(os.Stderr, "Searching builds...\n")Why: Users pipe stdout to files/grep. Progress messages should go to stderr.
Wrong:
buildCount := cached.BuildCounts[key] // Panic if BuildCounts is nil!Right:
if cached.BuildCounts == nil {
cached.BuildCounts = make(map[string]int)
}
buildCount := cached.BuildCounts[key]Why: Old cache files may not have newer fields. Backwards compatibility matters.
Wrong:
// Check much later in code, after expensive operations
if *pipeline == "" && !*allPipelines { ... }Right:
// Validate immediately after flag.Parse() (lines ~123-145)
if *pipeline == "" && !*allPipelines {
fmt.Fprintf(os.Stderr, "Error: either --pipeline or --all-pipelines is required\n")
os.Exit(1)
}Why: Fail fast with clear errors before doing any work.
- No bulk API: Can't get "all builds across all pipelines" in one call
- Sequential discovery: Must enumerate pipelines → jobs → builds
- Rate limiting: Be respectful, use caching aggressively
- Metadata TTL = 5 minutes: Balance freshness vs performance
- Build log TTL = 24 hours: Builds don't change once completed
- No LRU eviction: Keep it simple, rely on TTL expiration
- Default parallelism = 5: Conservative to avoid overwhelming Concourse
- User configurable:
--parallelflag for advanced users - Semaphore pattern: Limit concurrent API calls (line ~491)
If users report weird behavior:
# Clear all caches
./fly-search --clear-cache
# Rebuild from scratch
./fly-search -t TARGET -p PIPELINE -s "pattern"If searches are slow:
- Check cache hit rate (look for "Using cached" messages)
- Verify cache location is writable:
~/.fly-search/cache/ - Check cache file sizes:
du -sh ~/.fly-search/cache/ - Consider increasing
--parallelvalue
If tool crashes with OOM:
- Reduce
--countvalue (fewer builds = less memory) - Reduce
--parallelvalue (fewer concurrent fetches) - Use specific pipelines instead of
--all-pipelines
Potential improvements (not yet implemented):
- Persistent metadata cache with longer TTL (hour+ with smart invalidation)
- LRU cache eviction policy instead of pure TTL
- Incremental cache updates (only fetch new builds)
- Build status-aware caching (running builds = don't cache)
- Parallel pipeline discovery (currently sequential)
- Interactive mode with fuzzy search
- Real-time log streaming for running builds
- Export to CSV/HTML formats
- Configuration file support (~/.fly-search/config.yaml)
When asking for help or reporting issues:
- Include version info: Show
git describeor commit hash - Show full command: Include all flags used
- Include timing: How long did it take?
- Check cache state:
ls -lh ~/.fly-search/cache/ - Run with verbose logging: (when we add
-vflag)
- README.md: User-facing documentation with examples
- main.go: Primary source file, well-commented
- Concourse API docs: https://concourse-ci.org/api.html
Last Updated: 2025-12-24 Maintainer: AI-assisted development Go Version: 1.16+