From 53358ae409150f9b8e4c4dfbebd59d6c9e014be1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:19:28 +0000 Subject: [PATCH 1/5] Initial plan From 993efe9f137397ab12b882b388c346428ce2c4e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:26:30 +0000 Subject: [PATCH 2/5] Plan: enable go-gh disk cache for repository features clients Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/skills/agentic-workflows/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md index 22e2accc575..995e0a670cc 100644 --- a/.github/skills/agentic-workflows/SKILL.md +++ b/.github/skills/agentic-workflows/SKILL.md @@ -31,6 +31,7 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/debug-agentic-workflow.md` - `.github/aw/dependabot.md` - `.github/aw/deployment-status.md` +- `.github/aw/designer-mappings.md` - `.github/aw/designer.md` - `.github/aw/evals.md` - `.github/aw/experiments.md` @@ -100,4 +101,3 @@ After loading the matching workflow prompt or skill, follow it directly: - Design long-running multi-agent research workflows: `.github/aw/multi-agent-research.md` When the task involves OTEL, OTLP, traces, observability backends, or telemetry-driven analysis, also read and follow `skills/otel-queries/SKILL.md` after loading the matching workflow prompt or skill. - From 60634098bc7eae5a3f82cc4963b4ebe6db5ad476 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:32:14 +0000 Subject: [PATCH 3/5] Enable go-gh disk cache for repository features API calls Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../content/docs/troubleshooting/debugging.md | 8 ++++++ .../repository_features_validation.go | 27 ++++++++++++++++--- .../repository_features_validation_test.go | 2 +- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/src/content/docs/troubleshooting/debugging.md b/docs/src/content/docs/troubleshooting/debugging.md index 2062d2d3ecf..0919cbada48 100644 --- a/docs/src/content/docs/troubleshooting/debugging.md +++ b/docs/src/content/docs/troubleshooting/debugging.md @@ -137,6 +137,14 @@ DEBUG=cli:* gh aw audit 12345678 # CLI-specific DEBUG=workflow:*,cli:* gh aw compile # multiple packages ``` +### Enable GitHub API Request Logging + +`gh aw` uses go-gh's native REST/GraphQL clients for all GitHub API calls. Setting `GH_DEBUG=api` prints verbose request/response details for those calls to stderr, with no extra flags needed: + +```bash +GH_DEBUG=api gh aw compile my-workflow +``` + ### Enable GitHub Actions Debug Logging Add an `ACTIONS_STEP_DEBUG` repository secret set to `true` (**Settings → Secrets and variables → Actions**), then re-run the workflow for verbose step-level logging in the Actions UI. diff --git a/pkg/workflow/repository_features_validation.go b/pkg/workflow/repository_features_validation.go index 3b3ccaa60cb..20423fbba9c 100644 --- a/pkg/workflow/repository_features_validation.go +++ b/pkg/workflow/repository_features_validation.go @@ -20,10 +20,13 @@ // # Validation Pattern: Feature Detection with Caching // // Repository feature validation uses a caching pattern to amortize expensive API calls: -// - sync.Map for thread-safe cache storage +// - sync.Map for thread-safe, same-process cache storage // - sync.Once for single-fetch guarantee // - Atomic LoadOrStore for race-free caching // - Separate logged cache to avoid duplicate success messages +// - go-gh's disk-backed HTTP response cache (EnableCache/CacheTTL) as a second layer, +// persisting results across separate CLI invocations (e.g. repeated runs in the same +// CI workflow), on top of the in-process sync.Map fast path // // # When to Add Validation Here // @@ -58,6 +61,13 @@ import ( // (mirrors the copilot-billing probe timeout). const repositoryFeaturesTimeout = 3 * time.Second +// repositoryFeaturesCacheTTL controls how long go-gh's disk-backed HTTP response cache +// keeps repository feature lookups (discussions/issues enablement) valid. These settings +// rarely change, so persisting results across process invocations (not just within a +// single process's in-memory sync.Map cache below) meaningfully reduces redundant API +// calls when the CLI is invoked repeatedly, e.g. across steps in the same CI workflow. +const repositoryFeaturesCacheTTL = 5 * time.Minute + var repositoryFeaturesLog = logger.New("workflow:repository_features_validation") // checkRepositoryHasDiscussionsQuery is a hardcoded static GraphQL query template used to check @@ -275,7 +285,12 @@ func checkRepositoryHasDiscussionsUncached(repo string) (bool, error) { owner, name := parts[0], parts[1] // Use native GraphQL client — no gh binary dependency, native context/cancel support. - client, err := api.DefaultGraphQLClient() + // EnableCache persists the (rarely-changing) discussions-enabled lookup to go-gh's + // disk-backed HTTP cache so repeated CLI invocations don't re-query the API. + client, err := api.NewGraphQLClient(api.ClientOptions{ + EnableCache: true, + CacheTTL: repositoryFeaturesCacheTTL, + }) if err != nil { return false, fmt.Errorf("failed to create GraphQL client: %w", err) } @@ -307,8 +322,12 @@ func checkRepositoryHasIssues(repo string, verbose bool) (bool, error) { // checkRepositoryHasIssuesUncached checks if a repository has issues enabled (no caching) func checkRepositoryHasIssuesUncached(repo string) (bool, error) { - // Create REST client - client, err := api.DefaultRESTClient() + // Create REST client. EnableCache persists the (rarely-changing) has-issues lookup to + // go-gh's disk-backed HTTP cache so repeated CLI invocations don't re-query the API. + client, err := api.NewRESTClient(api.ClientOptions{ + EnableCache: true, + CacheTTL: repositoryFeaturesCacheTTL, + }) if err != nil { return false, fmt.Errorf("failed to create REST client: %w", err) } diff --git a/pkg/workflow/repository_features_validation_test.go b/pkg/workflow/repository_features_validation_test.go index 3797accaae7..80485acab23 100644 --- a/pkg/workflow/repository_features_validation_test.go +++ b/pkg/workflow/repository_features_validation_test.go @@ -180,7 +180,7 @@ func TestCheckRepositoryInvalidFormat(t *testing.T) { func TestCheckRepositoryHasIssuesUncached(t *testing.T) { // Test the REST client code path directly - // This test exercises the api.DefaultRESTClient() and client.Get() path + // This test exercises the api.NewRESTClient() (with disk cache enabled) and client.Get() path repo := "github/gh-aw" hasIssues, err := checkRepositoryHasIssuesUncached(repo) From 8a74e34d88242c72cf003a0efe6792c3240d9be8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:33:41 +0000 Subject: [PATCH 4/5] Add deterministic disk-cache tests for repository feature checks Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../content/docs/troubleshooting/debugging.md | 2 +- .../repository_features_validation.go | 28 ++-- ...pository_features_validation_cache_test.go | 127 ++++++++++++++++++ .../repository_features_validation_test.go | 2 +- 4 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 pkg/workflow/repository_features_validation_cache_test.go diff --git a/docs/src/content/docs/troubleshooting/debugging.md b/docs/src/content/docs/troubleshooting/debugging.md index 0919cbada48..e47e65b887c 100644 --- a/docs/src/content/docs/troubleshooting/debugging.md +++ b/docs/src/content/docs/troubleshooting/debugging.md @@ -139,7 +139,7 @@ DEBUG=workflow:*,cli:* gh aw compile # multiple packages ### Enable GitHub API Request Logging -`gh aw` uses go-gh's native REST/GraphQL clients for all GitHub API calls. Setting `GH_DEBUG=api` prints verbose request/response details for those calls to stderr, with no extra flags needed: +`gh aw` uses go-gh's native REST/GraphQL clients for many GitHub API calls. Setting `GH_DEBUG=api` prints verbose request/response details for calls made through those go-gh clients, with no extra flags needed: ```bash GH_DEBUG=api gh aw compile my-workflow diff --git a/pkg/workflow/repository_features_validation.go b/pkg/workflow/repository_features_validation.go index 20423fbba9c..f55ed077ab2 100644 --- a/pkg/workflow/repository_features_validation.go +++ b/pkg/workflow/repository_features_validation.go @@ -275,15 +275,10 @@ func checkRepositoryHasDiscussions(repo string, verbose bool) (bool, error) { return features.HasDiscussions, nil } -// checkRepositoryHasDiscussionsUncached checks if a repository has discussions enabled (no caching) +// checkRepositoryHasDiscussionsUncached checks if a repository has discussions enabled, bypassing +// only the in-process repositoryFeaturesCache/repositoryFeaturesLoggedCache layers. +// The underlying go-gh client still uses disk-backed HTTP response caching when enabled. func checkRepositoryHasDiscussionsUncached(repo string) (bool, error) { - // Split repo into owner and name - parts := strings.SplitN(repo, "/", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return false, fmt.Errorf("invalid repository format: %s. Expected format: owner/repo. Example: github/gh-aw", repo) - } - owner, name := parts[0], parts[1] - // Use native GraphQL client — no gh binary dependency, native context/cancel support. // EnableCache persists the (rarely-changing) discussions-enabled lookup to go-gh's // disk-backed HTTP cache so repeated CLI invocations don't re-query the API. @@ -294,6 +289,19 @@ func checkRepositoryHasDiscussionsUncached(repo string) (bool, error) { if err != nil { return false, fmt.Errorf("failed to create GraphQL client: %w", err) } + return checkRepositoryHasDiscussionsUncachedWithClient(repo, client) +} + +// checkRepositoryHasDiscussionsUncachedWithClient is the testable core of +// checkRepositoryHasDiscussionsUncached. It accepts an injectable GraphQL client so +// unit tests can assert cache behavior without live credentials. +func checkRepositoryHasDiscussionsUncachedWithClient(repo string, client *api.GraphQLClient) (bool, error) { + // Split repo into owner and name + parts := strings.SplitN(repo, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return false, fmt.Errorf("invalid repository format: %s. Expected format: owner/repo. Example: github/gh-aw", repo) + } + owner, name := parts[0], parts[1] var response struct { Repository struct { @@ -320,7 +328,9 @@ func checkRepositoryHasIssues(repo string, verbose bool) (bool, error) { return features.HasIssues, nil } -// checkRepositoryHasIssuesUncached checks if a repository has issues enabled (no caching) +// checkRepositoryHasIssuesUncached checks if a repository has issues enabled, bypassing +// only the in-process repositoryFeaturesCache/repositoryFeaturesLoggedCache layers. +// The underlying go-gh client still uses disk-backed HTTP response caching when enabled. func checkRepositoryHasIssuesUncached(repo string) (bool, error) { // Create REST client. EnableCache persists the (rarely-changing) has-issues lookup to // go-gh's disk-backed HTTP cache so repeated CLI invocations don't re-query the API. diff --git a/pkg/workflow/repository_features_validation_cache_test.go b/pkg/workflow/repository_features_validation_cache_test.go new file mode 100644 index 00000000000..a9433f9b71a --- /dev/null +++ b/pkg/workflow/repository_features_validation_cache_test.go @@ -0,0 +1,127 @@ +//go:build !integration + +package workflow + +import ( + "io" + "net/http" + "strings" + "sync/atomic" + "testing" + + "github.com/cli/go-gh/v2/pkg/api" +) + +type countingRoundTripper struct { + callCount int32 + body string +} + +func (c *countingRoundTripper) RoundTrip(*http.Request) (*http.Response, error) { + atomic.AddInt32(&c.callCount, 1) + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(c.body)), + }, nil +} + +func TestCheckRepositoryHasDiscussionsUncachedWithClient_UsesDiskCacheAcrossClients(t *testing.T) { + cacheDir := t.TempDir() + + newClient := func(rt http.RoundTripper) *api.GraphQLClient { + t.Helper() + client, err := api.NewGraphQLClient(api.ClientOptions{ + Host: "github.com", + AuthToken: "test-token", + EnableCache: true, + CacheTTL: repositoryFeaturesCacheTTL, + CacheDir: cacheDir, + Transport: rt, + }) + if err != nil { + t.Fatalf("failed to create GraphQL client: %v", err) + } + return client + } + + transport1 := &countingRoundTripper{ + body: `{"data":{"repository":{"hasDiscussionsEnabled":true}}}`, + } + + hasDiscussions, err := checkRepositoryHasDiscussionsUncachedWithClient("github/gh-aw", newClient(transport1)) + if err != nil { + t.Fatalf("first query failed: %v", err) + } + if !hasDiscussions { + t.Fatal("expected discussions to be enabled") + } + if got := atomic.LoadInt32(&transport1.callCount); got != 1 { + t.Fatalf("expected first client to hit transport once, got %d", got) + } + + transport2 := &countingRoundTripper{ + body: `{"data":{"repository":{"hasDiscussionsEnabled":true}}}`, + } + + hasDiscussions, err = checkRepositoryHasDiscussionsUncachedWithClient("github/gh-aw", newClient(transport2)) + if err != nil { + t.Fatalf("second query failed: %v", err) + } + if !hasDiscussions { + t.Fatal("expected discussions to be enabled") + } + if got := atomic.LoadInt32(&transport2.callCount); got != 0 { + t.Fatalf("expected second client to use disk cache (no transport calls), got %d", got) + } +} + +func TestCheckRepositoryHasIssuesUncachedWithClient_UsesDiskCacheAcrossClients(t *testing.T) { + cacheDir := t.TempDir() + + newClient := func(rt http.RoundTripper) *api.RESTClient { + t.Helper() + client, err := api.NewRESTClient(api.ClientOptions{ + Host: "github.com", + AuthToken: "test-token", + EnableCache: true, + CacheTTL: repositoryFeaturesCacheTTL, + CacheDir: cacheDir, + Transport: rt, + }) + if err != nil { + t.Fatalf("failed to create REST client: %v", err) + } + return client + } + + transport1 := &countingRoundTripper{ + body: `{"has_issues":true}`, + } + + hasIssues, err := checkRepositoryHasIssuesUncachedWithClient("github/gh-aw", newClient(transport1)) + if err != nil { + t.Fatalf("first query failed: %v", err) + } + if !hasIssues { + t.Fatal("expected issues to be enabled") + } + if got := atomic.LoadInt32(&transport1.callCount); got != 1 { + t.Fatalf("expected first client to hit transport once, got %d", got) + } + + transport2 := &countingRoundTripper{ + body: `{"has_issues":true}`, + } + + hasIssues, err = checkRepositoryHasIssuesUncachedWithClient("github/gh-aw", newClient(transport2)) + if err != nil { + t.Fatalf("second query failed: %v", err) + } + if !hasIssues { + t.Fatal("expected issues to be enabled") + } + if got := atomic.LoadInt32(&transport2.callCount); got != 0 { + t.Fatalf("expected second client to use disk cache (no transport calls), got %d", got) + } +} diff --git a/pkg/workflow/repository_features_validation_test.go b/pkg/workflow/repository_features_validation_test.go index 80485acab23..de00f549712 100644 --- a/pkg/workflow/repository_features_validation_test.go +++ b/pkg/workflow/repository_features_validation_test.go @@ -180,7 +180,7 @@ func TestCheckRepositoryInvalidFormat(t *testing.T) { func TestCheckRepositoryHasIssuesUncached(t *testing.T) { // Test the REST client code path directly - // This test exercises the api.NewRESTClient() (with disk cache enabled) and client.Get() path + // This test exercises the api.NewRESTClient() (with disk cache enabled) and client.DoWithContext() path repo := "github/gh-aw" hasIssues, err := checkRepositoryHasIssuesUncached(repo) From a9ff94548ca62e08a1d862017d0a21e604d38ce0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:41:50 +0000 Subject: [PATCH 5/5] Fix branch check failures and restore repo format validation order Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../agentic_workflows_fallback_aw_files.json | 1 + .../js/exchange_otlp_workload_identity.cjs | 1 + .../repository_features_validation.go | 30 +++++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/cli/data/agentic_workflows_fallback_aw_files.json b/pkg/cli/data/agentic_workflows_fallback_aw_files.json index b1f63d9fc3c..7d1f03c07d6 100644 --- a/pkg/cli/data/agentic_workflows_fallback_aw_files.json +++ b/pkg/cli/data/agentic_workflows_fallback_aw_files.json @@ -15,6 +15,7 @@ "debug-agentic-workflow.md", "dependabot.md", "deployment-status.md", + "designer-mappings.md", "designer.md", "evals.md", "experiments.md", diff --git a/pkg/workflow/js/exchange_otlp_workload_identity.cjs b/pkg/workflow/js/exchange_otlp_workload_identity.cjs index 49fbc63a218..4502bc65a22 100644 --- a/pkg/workflow/js/exchange_otlp_workload_identity.cjs +++ b/pkg/workflow/js/exchange_otlp_workload_identity.cjs @@ -1,4 +1,5 @@ // @ts-check +// @safe-outputs-exempt SEC-004 — "body" references are HTTP transport payloads for OAuth token exchange, not GitHub content /** * Exchanges a GitHub OIDC token for a Google Cloud access token using * Workload Identity Federation, optionally impersonating a service account. diff --git a/pkg/workflow/repository_features_validation.go b/pkg/workflow/repository_features_validation.go index f55ed077ab2..cc7cd57163f 100644 --- a/pkg/workflow/repository_features_validation.go +++ b/pkg/workflow/repository_features_validation.go @@ -279,6 +279,10 @@ func checkRepositoryHasDiscussions(repo string, verbose bool) (bool, error) { // only the in-process repositoryFeaturesCache/repositoryFeaturesLoggedCache layers. // The underlying go-gh client still uses disk-backed HTTP response caching when enabled. func checkRepositoryHasDiscussionsUncached(repo string) (bool, error) { + if err := validateRepositoryName(repo); err != nil { + return false, err + } + // Use native GraphQL client — no gh binary dependency, native context/cancel support. // EnableCache persists the (rarely-changing) discussions-enabled lookup to go-gh's // disk-backed HTTP cache so repeated CLI invocations don't re-query the API. @@ -296,12 +300,10 @@ func checkRepositoryHasDiscussionsUncached(repo string) (bool, error) { // checkRepositoryHasDiscussionsUncached. It accepts an injectable GraphQL client so // unit tests can assert cache behavior without live credentials. func checkRepositoryHasDiscussionsUncachedWithClient(repo string, client *api.GraphQLClient) (bool, error) { - // Split repo into owner and name - parts := strings.SplitN(repo, "/", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return false, fmt.Errorf("invalid repository format: %s. Expected format: owner/repo. Example: github/gh-aw", repo) + owner, name, err := parseRepositoryName(repo) + if err != nil { + return false, err } - owner, name := parts[0], parts[1] var response struct { Repository struct { @@ -332,6 +334,10 @@ func checkRepositoryHasIssues(repo string, verbose bool) (bool, error) { // only the in-process repositoryFeaturesCache/repositoryFeaturesLoggedCache layers. // The underlying go-gh client still uses disk-backed HTTP response caching when enabled. func checkRepositoryHasIssuesUncached(repo string) (bool, error) { + if err := validateRepositoryName(repo); err != nil { + return false, err + } + // Create REST client. EnableCache persists the (rarely-changing) has-issues lookup to // go-gh's disk-backed HTTP cache so repeated CLI invocations don't re-query the API. client, err := api.NewRESTClient(api.ClientOptions{ @@ -364,3 +370,17 @@ func checkRepositoryHasIssuesUncachedWithClient(repo string, client *api.RESTCli return response.HasIssues, nil } + +func validateRepositoryName(repo string) error { + _, _, err := parseRepositoryName(repo) + return err +} + +func parseRepositoryName(repo string) (owner string, name string, err error) { + parts := strings.SplitN(repo, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("invalid repository format: %s. Expected format: owner/repo. Example: github/gh-aw", repo) + } + + return parts[0], parts[1], nil +}