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
26 changes: 22 additions & 4 deletions pkg/errorutil/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@
// returned by the GitHub API and gh CLI.
package errorutil

import "strings"
import (
"strings"

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

var errorutilLog = logger.New("errorutil:errors")

// IsNotFoundError reports whether err represents an HTTP 404 / "not found" response.
// It returns false when err is nil.
// The check is case-insensitive and matches both the numeric literal "404" and
// the phrase "not found", which covers all known forms returned by the GitHub API,
// the gh CLI, and the go-gh library.
func IsNotFoundError(err error) bool {
return containsErrorSubstring(err, "404", "not found")
matched := containsErrorSubstring(err, "404", "not found")
if matched {
errorutilLog.Printf("Classified error as not-found (404): %v", err)
}
return matched
}

// IsForbiddenError reports whether err represents an HTTP 403 / "forbidden" response.
Expand All @@ -19,7 +29,11 @@ func IsNotFoundError(err error) bool {
// "HTTP 403" or "403 Forbidden", which avoids misclassifying unrelated errors
// like "forbidden character".
func IsForbiddenError(err error) bool {
return containsHTTPStatusSubstring(err, "403", "forbidden")
matched := containsHTTPStatusSubstring(err, "403", "forbidden")
if matched {
errorutilLog.Printf("Classified error as forbidden (403): %v", err)
}
return matched
}

// IsGoneError reports whether err represents an HTTP 410 / "gone" response.
Expand All @@ -28,7 +42,11 @@ func IsForbiddenError(err error) bool {
// "HTTP 410" or "410 Gone", which avoids misclassifying unrelated errors like
// "connection has gone away".
func IsGoneError(err error) bool {
return containsHTTPStatusSubstring(err, "410", "gone")
matched := containsHTTPStatusSubstring(err, "410", "gone")
if matched {
errorutilLog.Printf("Classified error as gone (410): %v", err)
}
return matched
}

// containsErrorSubstring reports whether err contains any of the provided
Expand Down
9 changes: 8 additions & 1 deletion pkg/jsonutil/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@ import (
"bytes"
"encoding/json"
"strings"

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

var jsonutilLog = logger.New("jsonutil:json")

// MarshalCompactNoHTMLEscape marshals a value to compact JSON without HTML escaping.
// It trims the trailing newline emitted by json.Encoder.
func MarshalCompactNoHTMLEscape(v any) (string, error) {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(v); err != nil {
jsonutilLog.Printf("MarshalCompactNoHTMLEscape encode failed: %v", err)
return "", err
}

return strings.TrimSuffix(buf.String(), "\n"), nil
result := strings.TrimSuffix(buf.String(), "\n")
jsonutilLog.Printf("MarshalCompactNoHTMLEscape produced %d bytes", len(result))
return result, nil
}
16 changes: 15 additions & 1 deletion pkg/syncutil/onceloader.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package syncutil

import "sync"
import (
"sync"

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

var syncutilLog = logger.New("syncutil:onceloader")

// OnceLoader caches the result of a fallible, expensive one-shot fetch.
// Safe for concurrent use; loader is invoked at most once.
Expand All @@ -17,8 +23,14 @@ func (o *OnceLoader[T]) Get(loader func() (T, error)) (T, error) {
defer o.mu.Unlock()

if !o.done {
syncutilLog.Print("OnceLoader.Get: cache miss, invoking loader")
o.result, o.err = loader()
o.done = true
if o.err != nil {
syncutilLog.Printf("OnceLoader.Get: loader failed: %v", o.err)
} else {
syncutilLog.Print("OnceLoader.Get: loader succeeded, result cached")
}
}

return o.result, o.err
Expand All @@ -29,6 +41,7 @@ func (o *OnceLoader[T]) Reset() {
o.mu.Lock()
defer o.mu.Unlock()

syncutilLog.Print("OnceLoader.Reset: clearing cached state")
var zero T
o.result = zero
o.err = nil
Expand All @@ -42,6 +55,7 @@ func (o *OnceLoader[T]) Override(result T, err error) {
o.mu.Lock()
defer o.mu.Unlock()

syncutilLog.Printf("OnceLoader.Override: storing cached value (err=%v)", err)
o.result = result
o.err = err
o.done = true
Expand Down
9 changes: 9 additions & 0 deletions pkg/workflow/safe_outputs_workflow_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"fmt"
"sort"

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

var safeOutputsWorkflowHelpersLog = logger.New("workflow:safe_outputs_workflow_helpers")

type workflowToolDefinitionOptions struct {
workflowName string
workflowInputs map[string]any
Expand All @@ -16,6 +19,7 @@ type workflowToolDefinitionOptions struct {

func generateWorkflowToolDefinition(opts workflowToolDefinitionOptions) map[string]any {
toolName := stringutil.NormalizeSafeOutputIdentifier(opts.workflowName)
safeOutputsWorkflowHelpersLog.Printf("Generating workflow tool definition: workflow=%s, tool=%s, inputs=%d", opts.workflowName, toolName, len(opts.workflowInputs))
description := fmt.Sprintf(opts.descriptionFormat, opts.workflowName)
properties, required := buildInputSchema(opts.workflowInputs, func(inputName string) string {
return fmt.Sprintf("Input parameter '%s' for workflow %s", inputName, opts.workflowName)
Expand All @@ -35,22 +39,27 @@ func generateWorkflowToolDefinition(opts workflowToolDefinitionOptions) map[stri
if len(required) > 0 {
sort.Strings(required)
tool["inputSchema"].(map[string]any)["required"] = required
safeOutputsWorkflowHelpersLog.Printf("Workflow tool %s has %d required inputs", toolName, len(required))
}

return tool
}

func resolveWorkflowExtension(fileResult *findWorkflowFileResult) (string, bool) {
if fileResult.lockExists {
safeOutputsWorkflowHelpersLog.Print("Resolved workflow extension: .lock.yml (lock file exists)")
return ".lock.yml", true
}
if fileResult.ymlExists {
safeOutputsWorkflowHelpersLog.Print("Resolved workflow extension: .yml (yml file exists)")
return ".yml", true
}
if fileResult.mdExists {
// .md-only: the workflow is a same-batch compilation target that will produce a .lock.yml
safeOutputsWorkflowHelpersLog.Print("Resolved workflow extension: .lock.yml (md-only, same-batch target)")
return ".lock.yml", true
}

safeOutputsWorkflowHelpersLog.Print("Failed to resolve workflow extension: no candidate files exist")
return "", false
}
9 changes: 9 additions & 0 deletions pkg/workflow/workflow_inputs_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,34 @@ package workflow
import (
"os"

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

var workflowInputsExtractorLog = logger.New("workflow:workflow_inputs_extractor")

func extractInputsFromYAML(workflowPath, trigger string) (map[string]any, error) {
workflowInputsExtractorLog.Printf("Extracting inputs from YAML: path=%s, trigger=%s", workflowPath, trigger)
workflow, err := readWorkflowYAML(workflowPath)
if err != nil {
workflowInputsExtractorLog.Printf("Failed to read workflow YAML: %v", err)
return nil, err
}

return extractInputsFromParsedWorkflow(workflow, trigger), nil
}

func extractInputsFromMarkdown(mdPath, trigger string) (map[string]any, error) {
workflowInputsExtractorLog.Printf("Extracting inputs from markdown: path=%s, trigger=%s", mdPath, trigger)
content, err := os.ReadFile(mdPath) // #nosec G304 -- mdPath is validated via isPathWithinDir in findWorkflowFile
if err != nil {
workflowInputsExtractorLog.Printf("Failed to read markdown file: %v", err)
return nil, err
}

result, err := parser.ExtractFrontmatterFromContent(string(content))
if err != nil || result == nil {
workflowInputsExtractorLog.Printf("No frontmatter extracted (err=%v), returning empty inputs", err)
return make(map[string]any), nil
}

Expand Down Expand Up @@ -60,5 +68,6 @@ func extractInputsFromParsedWorkflow(workflow map[string]any, trigger string) ma
return make(map[string]any)
}

workflowInputsExtractorLog.Printf("Found %d inputs for trigger %s", len(inputsMap), trigger)
return inputsMap
}
Loading