diff --git a/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml b/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml index ba22aecd25..4681884cd0 100644 --- a/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml +++ b/.github/workflows/go-file-size-reduction-project64.campaign.g.lock.yml @@ -599,7 +599,7 @@ jobs: - Files reduced to target size (primary): baseline 0 → target 100 over 90 days percent - Test coverage maintained (supporting): baseline 80 → target 80 over 7 days percent - Associated workflows: daily-file-diet - - Memory paths: memory/campaigns/go-file-size-reduction-project64-*/** + - Memory paths: memory/campaigns/go-file-size-reduction-project64/** - Metrics glob: `memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json` - Project URL: https://github.com/orgs/githubnext/projects/64 - Governance: max new items per run: 5 @@ -1624,7 +1624,7 @@ jobs: BRANCH_NAME: memory/campaigns MAX_FILE_SIZE: 10240 MAX_FILE_COUNT: 100 - FILE_GLOB_FILTER: "go-file-size-reduction-project64-*/**" + FILE_GLOB_FILTER: "go-file-size-reduction-project64/**" with: script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); diff --git a/.github/workflows/go-file-size-reduction-project64.campaign.g.md b/.github/workflows/go-file-size-reduction-project64.campaign.g.md index cb69b9a016..82af32ef7a 100644 --- a/.github/workflows/go-file-size-reduction-project64.campaign.g.md +++ b/.github/workflows/go-file-size-reduction-project64.campaign.g.md @@ -32,7 +32,7 @@ tools: repo-memory: - branch-name: memory/campaigns file-glob: - - go-file-size-reduction-project64-*/** + - go-file-size-reduction-project64/** id: campaigns --- @@ -49,7 +49,7 @@ This workflow orchestrates the 'Go File Size Reduction Campaign (Project 64)' ca - Files reduced to target size (primary): baseline 0 → target 100 over 90 days percent - Test coverage maintained (supporting): baseline 80 → target 80 over 7 days percent - Associated workflows: daily-file-diet -- Memory paths: memory/campaigns/go-file-size-reduction-project64-*/** +- Memory paths: memory/campaigns/go-file-size-reduction-project64/** - Metrics glob: `memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json` - Project URL: https://github.com/orgs/githubnext/projects/64 - Governance: max new items per run: 5 diff --git a/.github/workflows/go-file-size-reduction-project64.campaign.md b/.github/workflows/go-file-size-reduction-project64.campaign.md index d6417158c7..f294ba7bb5 100644 --- a/.github/workflows/go-file-size-reduction-project64.campaign.md +++ b/.github/workflows/go-file-size-reduction-project64.campaign.md @@ -9,7 +9,7 @@ workflows: - daily-file-diet tracker-label: "campaign:go-file-size-reduction-project64" memory-paths: - - "memory/campaigns/go-file-size-reduction-project64-*/**" + - "memory/campaigns/go-file-size-reduction-project64/**" metrics-glob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json" state: active tags: diff --git a/actions/setup/js/push_repo_memory.cjs b/actions/setup/js/push_repo_memory.cjs index 323a91d096..5ea28fb37e 100644 --- a/actions/setup/js/push_repo_memory.cjs +++ b/actions/setup/js/push_repo_memory.cjs @@ -180,6 +180,8 @@ async function main() { // If no explicit campaign ID, try to extract from patterns when memoryId is "campaigns" if (!campaignId && memoryId === "campaigns" && patterns.length > 0) { // Try to extract campaign ID from first pattern matching "/**" + // This only works for simple patterns without wildcards in the campaign ID portion + // For patterns like "campaign-id-*/**", use GH_AW_CAMPAIGN_ID environment variable const campaignMatch = /^([^*?/]+)\/\*\*/.exec(patterns[0]); if (campaignMatch) { campaignId = campaignMatch[1]; diff --git a/actions/setup/js/push_repo_memory.test.cjs b/actions/setup/js/push_repo_memory.test.cjs index 41ce2def49..b3e68bddeb 100644 --- a/actions/setup/js/push_repo_memory.test.cjs +++ b/actions/setup/js/push_repo_memory.test.cjs @@ -109,6 +109,26 @@ describe("push_repo_memory.cjs - globPatternToRegex helper", () => { expect(metricsRegex.test("security-q1/cursor.json")).toBe(false); }); + it("should match flexible campaign pattern for both dated and non-dated structures", () => { + // Pattern: go-file-size-reduction-project64*/** + // This should match BOTH: + // - go-file-size-reduction-project64-2025-12-31/ (with date suffix) + // - go-file-size-reduction-project64/ (without suffix) + const flexibleRegex = globPatternToRegex("go-file-size-reduction-project64*/**"); + + // Test dated structure (with suffix) + expect(flexibleRegex.test("go-file-size-reduction-project64-2025-12-31/cursor.json")).toBe(true); + expect(flexibleRegex.test("go-file-size-reduction-project64-2025-12-31/metrics/2025-12-31.json")).toBe(true); + + // Test non-dated structure (without suffix) + expect(flexibleRegex.test("go-file-size-reduction-project64/cursor.json")).toBe(true); + expect(flexibleRegex.test("go-file-size-reduction-project64/metrics/2025-12-31.json")).toBe(true); + + // Should not match other campaigns + expect(flexibleRegex.test("other-campaign/file.json")).toBe(false); + expect(flexibleRegex.test("security-q1/cursor.json")).toBe(false); + }); + it("should match multiple file extensions", () => { const patterns = ["*.json", "*.jsonl", "*.csv", "*.md"].map(globPatternToRegex); diff --git a/pkg/campaign/orchestrator.go b/pkg/campaign/orchestrator.go index 2aeefcc6b0..dacc8d7bbd 100644 --- a/pkg/campaign/orchestrator.go +++ b/pkg/campaign/orchestrator.go @@ -10,46 +10,49 @@ import ( var orchestratorLog = logger.New("campaign:orchestrator") -// extractFileGlobPattern extracts the file glob pattern from memory-paths or -// metrics-glob configuration. This pattern is used for the file-glob filter in +// convertStringsToAny converts a slice of strings to a slice of any +func convertStringsToAny(strings []string) []any { + result := make([]any, len(strings)) + for i, s := range strings { + result[i] = s + } + return result +} + +// extractFileGlobPatterns extracts all file glob patterns from memory-paths or +// metrics-glob configuration. These patterns are used for the file-glob filter in // repo-memory configuration to match files that the agent creates. // // For campaigns that use dated directory patterns (e.g., campaign-id-*/), this -// function preserves the wildcard pattern instead of using just the campaign ID. +// function preserves all wildcard patterns from memory-paths to support multiple +// directory structures (both dated and non-dated). // // Examples: -// - memory-paths: ["memory/campaigns/project64-*/**"] -> "project64-*/**" -// - metrics-glob: "memory/campaigns/project64-*/metrics/*.json" -> "project64-*/**" -// - no patterns with wildcards -> "project64/**" (fallback to ID) -func extractFileGlobPattern(spec *CampaignSpec) string { - // Try to extract pattern from memory-paths first - // Prefer patterns with wildcards in the directory name (e.g., campaign-id-*) - var firstValidPattern string +// - memory-paths: ["memory/campaigns/project64-*/**", "memory/campaigns/project64/**"] +// -> ["project64-*/**", "project64/**"] +// - memory-paths: ["memory/campaigns/project64-*/**"] -> ["project64-*/**"] +// - metrics-glob: "memory/campaigns/project64-*/metrics/*.json" -> ["project64-*/**"] +// - no patterns with wildcards -> ["project64/**"] (fallback to ID) +func extractFileGlobPatterns(spec *CampaignSpec) []string { + var patterns []string + + // Extract all patterns from memory-paths for _, memPath := range spec.MemoryPaths { // Remove "memory/campaigns/" prefix if present pattern := strings.TrimPrefix(memPath, "memory/campaigns/") // If pattern has both wildcards and slashes, it's a valid pattern if strings.Contains(pattern, "*") && strings.Contains(pattern, "/") { - // Check if wildcard is in the directory name (not just in **) - if strings.Contains(strings.Split(pattern, "/")[0], "*") { - // This pattern has a wildcard in the directory name - prefer it - orchestratorLog.Printf("Extracted file-glob pattern from memory-paths (with wildcard): %s", pattern) - return pattern - } - // Save this as a fallback valid pattern - if firstValidPattern == "" { - firstValidPattern = pattern - } + patterns = append(patterns, pattern) + orchestratorLog.Printf("Extracted file-glob pattern from memory-paths: %s", pattern) } } - // If we found a valid pattern (even without directory wildcard), use it - if firstValidPattern != "" { - orchestratorLog.Printf("Extracted file-glob pattern from memory-paths: %s", firstValidPattern) - return firstValidPattern + // If we found patterns from memory-paths, return them + if len(patterns) > 0 { + return patterns } - // Try to extract pattern from metrics-glob + // Try to extract pattern from metrics-glob as fallback if spec.MetricsGlob != "" { pattern := strings.TrimPrefix(spec.MetricsGlob, "memory/campaigns/") if strings.Contains(pattern, "*") { @@ -57,7 +60,7 @@ func extractFileGlobPattern(spec *CampaignSpec) string { if idx := strings.Index(pattern, "/metrics/"); idx > 0 { basePattern := pattern[:idx] + "/**" orchestratorLog.Printf("Extracted file-glob pattern from metrics-glob: %s", basePattern) - return basePattern + return []string{basePattern} } } } @@ -65,7 +68,7 @@ func extractFileGlobPattern(spec *CampaignSpec) string { // Fallback to simple ID-based pattern fallbackPattern := fmt.Sprintf("%s/**", spec.ID) orchestratorLog.Printf("Using fallback file-glob pattern: %s", fallbackPattern) - return fallbackPattern + return []string{fallbackPattern} } // BuildOrchestrator constructs a minimal agentic workflow representation for a @@ -259,9 +262,9 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W orchestratorLog.Printf("Campaign orchestrator '%s' built successfully with safe outputs enabled", spec.ID) - // Extract file-glob pattern from memory-paths or metrics-glob to support - // dated campaign directory patterns like "campaign-id-*/**" - fileGlobPattern := extractFileGlobPattern(spec) + // Extract file-glob patterns from memory-paths or metrics-glob to support + // multiple directory structures (e.g., both dated "campaign-id-*/**" and non-dated "campaign-id/**") + fileGlobPatterns := extractFileGlobPatterns(spec) data := &workflow.WorkflowData{ Name: name, @@ -283,7 +286,7 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W map[string]any{ "id": "campaigns", "branch-name": "memory/campaigns", - "file-glob": []any{fileGlobPattern}, + "file-glob": convertStringsToAny(fileGlobPatterns), }, }, "bash": []any{"*"}, diff --git a/pkg/campaign/orchestrator_test.go b/pkg/campaign/orchestrator_test.go index 021204538a..645ea4315e 100644 --- a/pkg/campaign/orchestrator_test.go +++ b/pkg/campaign/orchestrator_test.go @@ -313,13 +313,23 @@ func TestBuildOrchestrator_GovernanceOverridesSafeOutputMaxima(t *testing.T) { } } -func TestExtractFileGlobPattern(t *testing.T) { +func TestExtractFileGlobPatterns(t *testing.T) { tests := []struct { name string spec *CampaignSpec - expectedGlob string + expectedGlobs []string expectedLogMsg string }{ + { + name: "flexible pattern matching both dated and non-dated", + spec: &CampaignSpec{ + ID: "go-file-size-reduction-project64", + MemoryPaths: []string{"memory/campaigns/go-file-size-reduction-project64*/**"}, + MetricsGlob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json", + }, + expectedGlobs: []string{"go-file-size-reduction-project64*/**"}, + expectedLogMsg: "Extracted file-glob pattern from memory-paths", + }, { name: "dated pattern in memory-paths", spec: &CampaignSpec{ @@ -327,7 +337,20 @@ func TestExtractFileGlobPattern(t *testing.T) { MemoryPaths: []string{"memory/campaigns/go-file-size-reduction-project64-*/**"}, MetricsGlob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json", }, - expectedGlob: "go-file-size-reduction-project64-*/**", + expectedGlobs: []string{"go-file-size-reduction-project64-*/**"}, + expectedLogMsg: "Extracted file-glob pattern from memory-paths", + }, + { + name: "multiple patterns in memory-paths", + spec: &CampaignSpec{ + ID: "go-file-size-reduction-project64", + MemoryPaths: []string{ + "memory/campaigns/go-file-size-reduction-project64-*/**", + "memory/campaigns/go-file-size-reduction-project64/**", + }, + MetricsGlob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json", + }, + expectedGlobs: []string{"go-file-size-reduction-project64-*/**", "go-file-size-reduction-project64/**"}, expectedLogMsg: "Extracted file-glob pattern from memory-paths", }, { @@ -336,7 +359,7 @@ func TestExtractFileGlobPattern(t *testing.T) { ID: "go-file-size-reduction-project64", MetricsGlob: "memory/campaigns/go-file-size-reduction-project64-*/metrics/*.json", }, - expectedGlob: "go-file-size-reduction-project64-*/**", + expectedGlobs: []string{"go-file-size-reduction-project64-*/**"}, expectedLogMsg: "Extracted file-glob pattern from metrics-glob", }, { @@ -345,15 +368,15 @@ func TestExtractFileGlobPattern(t *testing.T) { ID: "simple-campaign", MemoryPaths: []string{"memory/campaigns/simple-campaign/**"}, }, - expectedGlob: "simple-campaign/**", - expectedLogMsg: "Using fallback file-glob pattern", + expectedGlobs: []string{"simple-campaign/**"}, + expectedLogMsg: "Extracted file-glob pattern from memory-paths", }, { name: "no memory paths or metrics glob", spec: &CampaignSpec{ ID: "minimal-campaign", }, - expectedGlob: "minimal-campaign/**", + expectedGlobs: []string{"minimal-campaign/**"}, expectedLogMsg: "Using fallback file-glob pattern", }, { @@ -365,16 +388,22 @@ func TestExtractFileGlobPattern(t *testing.T) { "memory/campaigns/multi-path-*/data/**", }, }, - expectedGlob: "multi-path-*/data/**", + expectedGlobs: []string{"multi-path-staging/**", "multi-path-*/data/**"}, expectedLogMsg: "Extracted file-glob pattern from memory-paths", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := extractFileGlobPattern(tt.spec) - if result != tt.expectedGlob { - t.Errorf("extractFileGlobPattern(%q) = %q, want %q", tt.spec.ID, result, tt.expectedGlob) + result := extractFileGlobPatterns(tt.spec) + if len(result) != len(tt.expectedGlobs) { + t.Errorf("extractFileGlobPatterns(%q) returned %d patterns, want %d", tt.spec.ID, len(result), len(tt.expectedGlobs)) + return + } + for i, expected := range tt.expectedGlobs { + if result[i] != expected { + t.Errorf("extractFileGlobPatterns(%q)[%d] = %q, want %q", tt.spec.ID, i, result[i], expected) + } } }) }