Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions pkg/campaign/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ import (
"path/filepath"
"strings"

"github.com/githubnext/gh-aw/pkg/logger"
"github.com/githubnext/gh-aw/pkg/workflow"
)

var statusLog = logger.New("campaign:status")

// ComputeCompiledState inspects the compiled state of all
// workflows referenced by a campaign. It returns:
//
Expand All @@ -22,6 +25,8 @@ import (
// "Missing workflow" - at least one referenced workflow markdown file does not exist
// "N/A" - campaign does not reference any workflows
func ComputeCompiledState(spec CampaignSpec, workflowsDir string) string {
statusLog.Printf("Computing compiled state for campaign '%s' with %d workflows", spec.ID, len(spec.Workflows))

if len(spec.Workflows) == 0 {
return "N/A"
}
Expand All @@ -35,21 +40,21 @@ func ComputeCompiledState(spec CampaignSpec, workflowsDir string) string {

mdInfo, err := os.Stat(mdPath)
if err != nil {
log.Printf("Workflow markdown not found for campaign '%s': %s", spec.ID, mdPath)
statusLog.Printf("Workflow markdown not found for campaign '%s': %s", spec.ID, mdPath)
missingAny = true
compiledAll = false
continue
}

lockInfo, err := os.Stat(lockPath)
if err != nil {
log.Printf("Lock file not found for workflow '%s' in campaign '%s': %s", wf, spec.ID, lockPath)
statusLog.Printf("Lock file not found for workflow '%s' in campaign '%s': %s", wf, spec.ID, lockPath)
compiledAll = false
continue
}

if mdInfo.ModTime().After(lockInfo.ModTime()) {
log.Printf("Lock file out of date for workflow '%s' in campaign '%s'", wf, spec.ID)
statusLog.Printf("Lock file out of date for workflow '%s' in campaign '%s'", wf, spec.ID)
compiledAll = false
}
}
Expand All @@ -75,6 +80,8 @@ type ghIssueOrPRState struct {
// If trackerLabel is empty or any errors occur, it falls back to zeros and
// logs at debug level instead of failing the command.
func FetchItemCounts(trackerLabel string) (issuesOpen, issuesClosed, prsOpen, prsMerged int) {
statusLog.Printf("Fetching item counts for tracker label: %s", trackerLabel)

if strings.TrimSpace(trackerLabel) == "" {
return 0, 0, 0, 0
}
Expand All @@ -94,10 +101,10 @@ func FetchItemCounts(trackerLabel string) (issuesOpen, issuesClosed, prsOpen, pr
}
}
} else if err != nil {
log.Printf("Failed to decode issue list for tracker label '%s': %v", trackerLabel, err)
statusLog.Printf("Failed to decode issue list for tracker label '%s': %v", trackerLabel, err)
}
} else if err != nil {
log.Printf("Failed to fetch issues for tracker label '%s': %v", trackerLabel, err)
statusLog.Printf("Failed to fetch issues for tracker label '%s': %v", trackerLabel, err)
}

// Pull requests
Expand All @@ -116,10 +123,10 @@ func FetchItemCounts(trackerLabel string) (issuesOpen, issuesClosed, prsOpen, pr
}
}
} else if err != nil {
log.Printf("Failed to decode PR list for tracker label '%s': %v", trackerLabel, err)
statusLog.Printf("Failed to decode PR list for tracker label '%s': %v", trackerLabel, err)
}
} else if err != nil {
log.Printf("Failed to fetch PRs for tracker label '%s': %v", trackerLabel, err)
statusLog.Printf("Failed to fetch PRs for tracker label '%s': %v", trackerLabel, err)
}

return issuesOpen, issuesClosed, prsOpen, prsMerged
Expand All @@ -130,6 +137,8 @@ func FetchItemCounts(trackerLabel string) (issuesOpen, issuesClosed, prsOpen, pr
// memory/campaigns branch. It is best-effort: errors are logged and
// treated as "no metrics" rather than failing the command.
func FetchMetricsFromRepoMemory(metricsGlob string) (*CampaignMetricsSnapshot, error) {
statusLog.Printf("Fetching metrics from repo memory with glob: %s", metricsGlob)

if strings.TrimSpace(metricsGlob) == "" {
return nil, nil
}
Expand All @@ -138,7 +147,7 @@ func FetchMetricsFromRepoMemory(metricsGlob string) (*CampaignMetricsSnapshot, e
cmd := exec.Command("git", "ls-tree", "-r", "--name-only", "memory/campaigns")
output, err := cmd.Output()
if err != nil {
log.Printf("Unable to list repo-memory branch for metrics (memory/campaigns): %v", err)
statusLog.Printf("Unable to list repo-memory branch for metrics (memory/campaigns): %v", err)
return nil, nil
}

Expand All @@ -151,7 +160,7 @@ func FetchMetricsFromRepoMemory(metricsGlob string) (*CampaignMetricsSnapshot, e
}
matched, err := path.Match(metricsGlob, pathStr)
if err != nil {
log.Printf("Invalid metrics_glob '%s': %v", metricsGlob, err)
statusLog.Printf("Invalid metrics_glob '%s': %v", metricsGlob, err)
return nil, nil
}
if matched {
Expand All @@ -175,13 +184,13 @@ func FetchMetricsFromRepoMemory(metricsGlob string) (*CampaignMetricsSnapshot, e
showCmd := exec.Command("git", "show", showArg)
fileData, err := showCmd.Output()
if err != nil {
log.Printf("Failed to read metrics file '%s' from memory/campaigns: %v", latest, err)
statusLog.Printf("Failed to read metrics file '%s' from memory/campaigns: %v", latest, err)
return nil, nil
}

var snapshot CampaignMetricsSnapshot
if err := json.Unmarshal(fileData, &snapshot); err != nil {
log.Printf("Failed to decode metrics JSON from '%s': %v", latest, err)
statusLog.Printf("Failed to decode metrics JSON from '%s': %v", latest, err)
return nil, nil
}

Expand All @@ -194,14 +203,16 @@ func FetchMetricsFromRepoMemory(metricsGlob string) (*CampaignMetricsSnapshot, e
//
// Errors are treated as "no cursor" rather than failing the command.
func FetchCursorFreshnessFromRepoMemory(cursorGlob string) (cursorPath string, cursorUpdatedAt string) {
statusLog.Printf("Fetching cursor freshness from repo memory with glob: %s", cursorGlob)

if strings.TrimSpace(cursorGlob) == "" {
return "", ""
}

cmd := exec.Command("git", "ls-tree", "-r", "--name-only", "memory/campaigns")
output, err := cmd.Output()
if err != nil {
log.Printf("Unable to list repo-memory branch for cursor (memory/campaigns): %v", err)
statusLog.Printf("Unable to list repo-memory branch for cursor (memory/campaigns): %v", err)
return "", ""
}

Expand All @@ -214,7 +225,7 @@ func FetchCursorFreshnessFromRepoMemory(cursorGlob string) (cursorPath string, c
}
matched, err := path.Match(cursorGlob, pathStr)
if err != nil {
log.Printf("Invalid cursor_glob '%s': %v", cursorGlob, err)
statusLog.Printf("Invalid cursor_glob '%s': %v", cursorGlob, err)
return "", ""
}
if matched {
Expand All @@ -238,7 +249,7 @@ func FetchCursorFreshnessFromRepoMemory(cursorGlob string) (cursorPath string, c
logCmd := exec.Command("git", "log", "-1", "--format=%cI", "memory/campaigns", "--", latest)
logOut, err := logCmd.Output()
if err != nil {
log.Printf("Failed to read cursor freshness for '%s' from memory/campaigns: %v", latest, err)
statusLog.Printf("Failed to read cursor freshness for '%s' from memory/campaigns: %v", latest, err)
return latest, ""
}

Expand All @@ -257,7 +268,7 @@ func BuildRuntimeStatus(spec CampaignSpec, workflowsDir string) CampaignRuntimeS
var metricsETA string
if strings.TrimSpace(spec.MetricsGlob) != "" {
if snapshot, err := FetchMetricsFromRepoMemory(spec.MetricsGlob); err != nil {
log.Printf("Failed to fetch metrics for campaign '%s': %v", spec.ID, err)
statusLog.Printf("Failed to fetch metrics for campaign '%s': %v", spec.ID, err)
} else if snapshot != nil {
metricsTasksTotal = snapshot.TasksTotal
metricsTasksCompleted = snapshot.TasksCompleted
Expand Down
18 changes: 11 additions & 7 deletions pkg/parser/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,42 @@ import (
"os"
"os/exec"
"strings"

"github.com/githubnext/gh-aw/pkg/logger"
)

var githubLog = logger.New("parser:github")

// GetGitHubToken attempts to get GitHub token from environment or gh CLI
func GetGitHubToken() (string, error) {
log.Print("Getting GitHub token")
githubLog.Print("Getting GitHub token")

// First try environment variable
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
log.Print("Found GITHUB_TOKEN environment variable")
githubLog.Print("Found GITHUB_TOKEN environment variable")
return token, nil
}
if token := os.Getenv("GH_TOKEN"); token != "" {
log.Print("Found GH_TOKEN environment variable")
githubLog.Print("Found GH_TOKEN environment variable")
return token, nil
}

// Fall back to gh auth token command
log.Print("Attempting to get token from gh auth token command")
githubLog.Print("Attempting to get token from gh auth token command")
cmd := exec.Command("gh", "auth", "token")
// Note: gh auth token should respect GH_HOST environment variable for enterprise
output, err := cmd.Output()
if err != nil {
log.Printf("Failed to get token from gh auth token: %v", err)
githubLog.Printf("Failed to get token from gh auth token: %v", err)
return "", fmt.Errorf("GITHUB_TOKEN environment variable not set and 'gh auth token' failed: %w", err)
}

token := strings.TrimSpace(string(output))
if token == "" {
log.Print("gh auth token returned empty token")
githubLog.Print("gh auth token returned empty token")
return "", fmt.Errorf("GITHUB_TOKEN environment variable not set and 'gh auth token' returned empty token")
}

log.Print("Successfully retrieved token from gh auth token")
githubLog.Print("Successfully retrieved token from gh auth token")
return token, nil
}
9 changes: 8 additions & 1 deletion pkg/parser/import_directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package parser
import (
"regexp"
"strings"

"github.com/githubnext/gh-aw/pkg/logger"
)

var importDirectiveLog = logger.New("parser:import_directive")

// IncludeDirectivePattern matches @include, @import (deprecated), or {{#import (new) directives
// The colon after #import is optional and ignored if present
var IncludeDirectivePattern = regexp.MustCompile(`^(?:@(?:include|import)(\?)?\s+(.+)|{{#import(\?)?\s*:?\s*(.+?)\s*}})$`)
Expand Down Expand Up @@ -32,6 +36,7 @@ func ParseImportDirective(line string) *ImportDirectiveMatch {

// Check if it's legacy syntax
isLegacy := LegacyIncludeDirectivePattern.MatchString(trimmedLine)
importDirectiveLog.Printf("Parsing import directive: legacy=%t, line=%s", isLegacy, trimmedLine)

var isOptional bool
var path string
Expand All @@ -48,10 +53,12 @@ func ParseImportDirective(line string) *ImportDirectiveMatch {
path = strings.TrimSpace(matches[4])
}

return &ImportDirectiveMatch{
match := &ImportDirectiveMatch{
IsOptional: isOptional,
Path: path,
IsLegacy: isLegacy,
Original: trimmedLine,
}
importDirectiveLog.Printf("Parsed import directive: path=%s, optional=%t, legacy=%t", path, isOptional, isLegacy)
return match
}
5 changes: 5 additions & 0 deletions pkg/parser/import_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import (
"strings"

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/logger"
)

var importErrorLog = logger.New("parser:import_error")

// ImportError represents an error that occurred during import resolution
type ImportError struct {
ImportPath string // The import path that failed (e.g., "nonexistent.md")
Expand All @@ -28,6 +31,8 @@ func (e *ImportError) Unwrap() error {

// FormatImportError formats an import error as a compilation error with source location
func FormatImportError(err *ImportError, yamlContent string) error {
importErrorLog.Printf("Formatting import error: path=%s, file=%s, line=%d", err.ImportPath, err.FilePath, err.Line)

lines := strings.Split(yamlContent, "\n")

// Create context lines around the error
Expand Down
7 changes: 7 additions & 0 deletions pkg/parser/yaml_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,28 @@ package parser
import (
"fmt"
"strings"

"github.com/githubnext/gh-aw/pkg/logger"
)

var yamlErrorLog = logger.New("parser:yaml_error")

// ExtractYAMLError extracts line and column information from YAML parsing errors
// frontmatterLineOffset is the line number where the frontmatter content begins in the document (1-based)
// This allows proper line number reporting when frontmatter is not at the beginning of the document
func ExtractYAMLError(err error, frontmatterLineOffset int) (line int, column int, message string) {
yamlErrorLog.Printf("Extracting YAML error information: offset=%d", frontmatterLineOffset)
errStr := err.Error()

// First try to extract from goccy/go-yaml's [line:column] format
line, column, message = extractFromGoccyFormat(errStr, frontmatterLineOffset)
if line > 0 || column > 0 {
yamlErrorLog.Printf("Extracted error location from goccy format: line=%d, column=%d", line, column)
return line, column, message
}

// Fallback to standard YAML error string parsing for other libraries
yamlErrorLog.Print("Falling back to string parsing for error location")
return extractFromStringParsing(errStr, frontmatterLineOffset)
}

Expand Down